I’ve been using lithium polymer (lipo) batteries since 2006, when I nervously shelled out £30 for a 3 cell 1600 mAh 10C HiModel lipo to power my EasyStar RC plane. I also spent about the same on a charger and balancer for it. Thankfully, all these things have come down a lot in price since then. I don’t think I’d expect to pay much more than £10 for an equivalent battery now.
Typical lipo batteries for large devices have multiple cells. A lipo cell has a no-load resting voltage of 4.2 V when fully charged and 3.7 V when discharged (you can take them a bit further, but they don’t last as many cycles).
It’s important that the cells within a battery are fairly evenly balanced, which is why most multi-cell lipos either have balancing circuitry built into them (e.g. laptop battery) or balance ports attached (e.g. RC planes)…
It just so happens that the commonly used JST-XH balance port connectors are the same size and spacing (0.1 inch) as the standard pinout on many PCBs. This is useful because it means we can use standard breadboarding wires to make connections – and even standard pin headers if we make a permanent stripboard attachment.
I have a little device that plugs into a lipo balance port and uses a 7 segment display to show the voltage of each cell in succession. It looks something like this one…
But for a number of years I’ve had a hankering to make one of my own. I’ve been working on my RasPiO Duino lately and decided it was time I got on with this little project. The lipo gauge I use has a tiny Atmel microcontroller on so the Duino’s ATMEGA328P-PU is more than up to the task.
Before We Start The Explanation
This project is quite complex compared to the examples that come with the RasPiO Duino. If you backed the RasPiO Duino KickStarter, you will be getting a much gentler introduction than this. With this post, I’m just doing what I tend to do on RasPi.TV, which is to blog “what I’m doing right now”. It’s not intended to be part of the RasPiO Duino materials, but could be considered an extra.
OK. Let’s get on with the explanation…
The Main Problem?
The biggest issue is that we are running the RasPiO Duino at 3.3 V, so the maximum Voltage we can measure with the onboard analog ports is 3.3 V. The balance port on cell 3 can reach 12.6 V, so we need a way of reducing this to <= 3.3 V. It just so happens that we can use a neat little electronics trick called a voltage divider or resistor divider. Who said resistance was futile? [caption id="attachment_7881" align="aligncenter" width="695"] Resistor divider for reducing our input voltage[/caption]
Yuck – Maths!
It’s not that bad actually. It follows Ohm’s law (Current = Voltage / Resistance, I = V / R) and allows us to choose resistor values that will adjust the voltage down to whatever we want. The proportion on the R2 side is ‘thrown away’ to GND and the other proportion goes to Vout.
I don’t like wasting battery power, so decided to use 10 kΩ resistors for R2. This means that we’re only wasting 3.3 V / 10,000 Ω = 0.00033 Amps, or 0.33 milliAmps to GND for each cell (1 mA total).
In an ideal world, we’d want to reduce all three of our maximum input voltages to exactly 3.3 V, to make full use of the ATMEGA’s 0-3.3 V measurement scale, but sometimes you have to compromise a bit, according to the stock of resistors you actually have.
Cell 1 dropping 4.2 V to exactly 3.3 V would require R1 = 2.725 kΩ
Cell 2, dropping 8.4 V to exactly 3.3 V would require R1 = 15.45 kΩ
Cell 3, dropping 12.6 V to exactly 3.3 V would require R1 = 27.9 kΩ
In practice, you want to err on the high side of resistance (lower voltage), so I picked the next highest that I had available in each case…
3.3 kΩ
18 kΩ
30 kΩ (I used a 10kΩ and 20kΩ in series)
This means that our maximum Vout values should be…
4.2 * 10 / 13.3 = 3.16 V
8.4 * 10 / 28 = 3.00 V
12.6 * 10 / 40 = 3.15 V
…but we can apply a correction (and calibration) in software, later on, to calculate the right values. But let’s not get ahead of ourselves. There’s a circuit to build first…
Here’s what the real thing looks like. Since we’re using an HDMIPi monitor, we can display all three cell Voltages at once…
And once I had proof of concept on the breadboard, I made a permanent version on a small scrap of stripboard I had lying around. It could have been about 6 holes shorter, but meh!
And here’s a view of the whole stripboard version with full on-screen output. First three lines are raw output from the Duino, next three lines are processed by the Pi in Python…
The stripboard version eliminates 7 out of 8 of the jumper wires, which makes it much simpler to use (as long as you keep your wits about you and plug the lipo in the right way round – note the large GND label).
So How Does It Work?
The RasPiO Duino is programmed to read analog ports A0-A2 six times. It discards the first reading, then averages the next five. The reason for chucking away the first reading is that it’s sometimes inaccurate (it’s something to do with multiplexing and capacitors that I once read about in the Adafruit Forums). It doesn’t cost us anything and doesn’t take very long, so I like to do it.
// RasPiO Duino lipo monitoring void setup() { Serial.begin(9600); //Start serial connection with computer } void loop() { // read analog pins 0-2 and report results by serial for (int adcPin = 0; adcPin < 3; adcPin++) { analogRead(adcPin); // first adc reading is discarded delay(20); int reading = 0; // now we read the pin 5 times for (int loop = 0; loop < 5; loop++) { reading += analogRead(adcPin); // add each value delay(20); } // now divide by 5 for average and convert to a voltage float voltage = reading * 3.29 / 5.0; voltage /= 1023.0; float adc = reading / 5.0; // send output to Pi through serial port Serial.print("ID");Serial.print(adcPin);Serial.print(" "); Serial.print(voltage);Serial.print(" V "); Serial.print(" ADC: "); Serial.println(adc); } delay(1000); // wait a second then read them all again }
Lines 3-6 deal with one-time setup of the microcontroller. In this case, we’re setting up the serial port connection.
Lines 8-30 are the main perpetual loop of the program.
Lines 11-12 do the intial read of the analog pin (which we ignore)
Lines 14-18 we read the analog pin 5 times and sum the results.
Lines 20-21 once we have five readings we’ll divide by 5 to average them, then convert to a Voltage by multiplying the result by 3.29 (measured Voltage of the Pi 3V3 rail) and dividing by 1023, which is the full scale of our 10 bit analog to digital converter.
So if our average ADC reading was, say, 987, the measured Voltage would be…
987 * 3.29 / 1023 = 3.17 V
Lines 24-27 deal with sending the data to the Pi via the serial port (Tx, Rx). At the Pi end, we use a Python script to display this. (More on that in a minute).
Line 29 – wait a second then do it all over again. This loop goes on forever.
(The sketches that come with the RasPiO Duino have a thorough explanation of what they do and how they work. They start really simply and build progressively. The idea is to get you far enough that you can start to do ‘your own thing’.)
So What About The Pi End?
You might have noticed, in the Fritzing diagram and the RasPiO Duino photo, there are 5 blue jumpers in place. The bottom 3 are for programming the Duino using the SPI pins (MOSI, MISO, SCLK). The top 2 connect the ATMEGA’s Tx/Rx pins to the Rx/Tx pins of the Pi. This enables the Duino to communicate (bi-directionally) with the Pi via the serial port. So we’re using a Python script to read the serial port and display the output on the screen.
import serial import subprocess import sys from time import sleep def print_there(x, y, text): # define function to overprint previous output sys.stdout.write("\x1b7\x1b[%d;%df%s\x1b8" % (x, y, text)) sys.stdout.flush() correction_factor = 1.33993 correction_factor2 = 2.83916 # 2.81944 for breadboard correction_factor3 = 4.02980 subprocess.Popen("clear", shell=True) # clear the screen to start while True: sport = serial.Serial("/dev/ttyAMA0", 9600, timeout=1) # open serial port try: # stops program failing if no serial data response = sport.readlines(None)[0:3] # read 3 lines of serial port data sport.close() except: sport.close() if not response: # stops program failing if no serial data print "no serial data read" sleep(0.5) continue # if no data, skip to top of loop and retry print_there(1,1,response[0]) # show raw ATMEGA output for A0-A2 print_there(2,1,response[1]) # A1 print_there(3,1,response[2]) # A2 # apply correction factors and format data for accurate display of # cell 1-3 voltages if response[0].startswith("ID0"): volts = float(response[0].split()[1]) * correction_factor output1 = ''.join(("Cell 1: ","{:.2f}".format(volts)," V ")) print_there(4,1,output1) if response[1].startswith("ID1"): volts2 = float(response[1].split()[1]) * correction_factor2 volts2_alone = volts2 - volts output2 = ''.join(("Cell 2: ","{:.2f}".format(volts2_alone)," V ")) print_there(5,1,output2) if response[2].startswith("ID2"): volts3 = float(response[2].split()[1]) * correction_factor3 volts3_alone = volts3 - volts2 output3 = ''.join(("Cell 3: ","{:.2f}".format(volts3_alone)," V ")) print_there(6,1,output3) sleep(1)
Lines 1-4 import the libraries that we need to use.
Lines 6-8 define a function that will print our updated values over the top of the previous ones. This avoids a distracting continuous scrolling on the screen.
Lines 10-12 set our calibration values (more on that in next part)
Line 14 clears the screen
Line 16 starts a perpetual loop
Lines 17-28 query the serial port in such a way that the program won’t ‘error out’ if there’s nothing to read. It will retry half a second later.
Lines 30-32 print the raw output from the ATMEGA microcontroller (needed for calibration, but useful anyway)
Lines 36-49 do the maths and print out our cell voltages nicely formatted as we want them on the screen. This part contains repetitive code, which could be optimised into a loop if you wanted to be really fussy.
Calibration?
Calibration is achieved by measuring the actual Voltages at the cells with a Voltmeter and dividing by the Voltage read by the analog ports of the Duino (tagged ID0-ID2).
Cell 1, it says ID0 3.03 V, the cell Voltage measured was 4.06 V, so the correction factor is 4.06 V / 3.03 V= 1.3399
Cell 2, it says ID1 2.86 V, the cell Voltage measured was 8.12 V, so the correction factor is 8.12 V / 2.86 V = 2.8391
Cell 3, it says ID2 3.02 V, the cell Voltage measured was 12.17 V, so the correction factor is 12.17 V / 3.02 V = 4.0298
These are the correction factors used in lines 8-10 of the Python script. We do our tweaking in the Python script so we don’t have to keep reflashing the ATMEGA chip. This is how the lipo monitor is calibrated to our specific multimeter. Obviously the calibration is only as accurate as the multimeter. But it’s plenty good enough for my purposes.
Further Enhancements?
You wouldn’t necessarily want to carry your HDMIPi around with you just to measure lipos, but you could easily log into your Pi from your mobile phone or tablet. You could even use the phone as a wifi hotspot for the Pi. Another option would be to add a small 16×2 or 16×4 character display, or a 7 segment display. With an onboard display, you could leave the Pi at home as well (once the Duino is programmed).
There’s lots of room left for further tweaking. That’s all part of the fun. After several weeks of working hard to make the RasPiO Duino happen, it’s been great fun to actually use it for something I’ve wanted to do for ages.
I guess
float voltage = reading * 3.29 / 5.0;
voltage /= 1023.0;
float adc = reading / 5.0;
would be a tiny bit neater as:
float adc = reading / 5.0;
float voltage = adc * 3.29 / 1023.0;
:-)
Nice ‘duino example though! Maybe you could also mention that the more the voltage is ‘reduced’ by the divider, the less accurate the final reading will be? I.e. the margin of error in the reading for Cell3 in your examples will be 3 times the margin of error in the reading for Cell1. (but probably still more than accurate enough for your intended purpose of course)
Andrew, I’d like to read up on the ‘voltage divider error proportional to voltage measured’ issue you mentioned, but I can’t find any references. Got a link that would point me in the right direction?
Well, I don’t have any reference, but the way I see it is that the “raw ADC pin” has an accuracy of +/- (3.3V / 1023) i.e. approx 3.3mV. So if you’re then using that to measure a “divided down” voltage, the accuracy of the input will be 3.3mV * correction factor. So for measuring your Cell3 voltage (with a 4x divider), you’ll only be measuring with an accuracy of approx +/- 13.2mV. But of course taking an average of 5 readings improves accuracy too.
This is probably all theoretical anyway, and may be too subtle a difference for the ATMEGA’s ADC to pick up?
I tried googling and found http://electronics.stackexchange.com/questions/23573/lpc2148-adc-problem-with-voltage-divider-for-battery which is interesting – I guess what I’m referring to above is the “quantization error”.
http://provideyourown.com/2012/secret-arduino-voltmeter-measure-battery-voltage/ is interesting too – I never knew that AVRs had an internal voltage reference!
This is precision, not accuracy and I think is the root of why I didn’t understand your comment very well.
Accuracy is “how close to the true value is this measurement”. http://en.wikipedia.org/wiki/Accuracy_and_precision
As far as I am aware, the voltage divider performs exactly the same way across a wide range of voltages (to more decimal places than any multimeter you could ever buy) so I don’t think your multiplication by 4 is valid.
Yes, there will be a tolerance on the resistors (+1% or +5% – whatever), but that is almost entirely mitigated by the fact that we are doing a calibration to a specific multimeter (which was the same one used to measure the Pi’s 3V3 rail at 3.29 V)
Yes. I would think that any errors introduced by the divider would be too small to measure (or care about).
It’s late. I’ll look at the links tomorrow :)
You’re right, I meant precision rather than accuracy – I didn’t realise the two terms had different meanings so apologies for the confusion. I’ve learnt something new :)
When using a 4x voltage divider, the quantisation step (of the pre-divider input) *is* 4 times bigger than when reading a ‘native’ 3.3V signal. But as you say this is probably beyond the limits of what you can measure with a regular multimeter anyway, so sorry for creating any unnecessary concern.
One of the articles I linked to above says that the bigger the value of resistors used in the divider (i.e. the smaller the current flowing into the ADC – just 0.33mA in your case), the longer the ADC reading takes. But again for your low-speed ADC example, that’s nothing to worry about.
I did Analytical Chemistry at Uni. Accuracy and precision (and measurement uncertainty) were term 1, lab 1. Pretty much the first thing we learnt. I had no idea before that either. (We did some simple electronics in term 3, which was my first real intro to ADCs, op-amps etc.)
I haven’t read the quantisation thing yet, but I can see what you mean by it. Effectively spreading the 1023 data points over 12.6V vs 4.2V would mean precision of 12.6/1023 rather than 4.2/1023. This would only be an issue if we needed precision better than 0.01 V (in which case, I think out best bet would be to use a 12-bit ADC).
But as you said, multiple readings helps to give you more “effective” precision. It’s always good to take multiple readings and average them. pretty much all instruments do this. As a result of all this I’ve found out how to increase the precision sent to serial from the ATMEGA to 3 decimal places, rather than the default of 2.
This has given me even more stable on-screen readings.
One question, why did you connect each voltage divider to ground? Couldn’t you have put the voltage divider between cells 3 and 2 to measure cell 3, cells 2 and 1 to measure cell 2 and cell 1 and ground to measure cell 1? That way all your voltage dividers would be made up of the same value resistors.
I didn’t think of it, but I think you might be right. I may well give this a try later on so I can work it out in my own head :)
That won’t work – ‘simple’ ADCs like those used in the ATMEGA chips need a common ground reference. To use different ground references for different ADC inputs, you’d need to use a differential ADC.
If you tried doing what you say with the ATMEGA, you’d have GND connected to both the negative end of Cell1 and Cell2, but the negative end of Cell2 is also the positive end of Cell1, i.e. you’d be creating a dangerous short circuit accross Cell1!
Yeah – I tried it nice and safely on a breadboard with a multimeter and it didn’t give me the values I expected, so I shelved it. :)
I thought I had successfully completed this battery monitor with my Raspberry Pi, as the 3 voltages were read fine while my 3S LiPo was not discharged. Then, I attempted to discharge the battery through its Dean’s Plug using a small motor. As soon as I tried to connect the GND balance port to the Pi’s GND, the GND wire instantly started to smoke and it melted. Why would this happen? Is the voltage not supposed to be measured while discharging?
I also used the MCP3008 ADC, as I do not have a raspio-duino.
If the wire smoked and melted, it’s almost certain that you had a short. Without seeing your wiring it’s impossible to say, but there MUST have been a wiring error. For the wire to melt there must have been significant current going through it.