Battery Monitor

This is an older project and no longer being works upon.


Battery Monitor

The Pi can do the task of a battery monitor with some extra (current shunt), millivolt amplifier and an analog to digital converter (the Arduino has analog inputs, but the Pi lacks this) or if galvanic isolation is required a Hall-effect sensor. The Hall-effect sensor outputs a signal at 2.5 V (+5 volt input power divided by two). This signal can be directly input to the analog to digital converter. 
Monitoring the battery bank and keeping track of the battery as a bank require constantly monitoring the current in or out of the battery. This is a real time task (suited for Arduino). However,  the Pi can easily do this several times a second which is good enough.  However, the power draw from a nano Arduino is very small and much smaller than a Pi. If power draw is of concern the Pi might be turned of to save energy. But the battery monitor need to be running at all times. Hence it might be a good idea to use a battery monitor with the smallest possible energy consumption. The above apply mostly to lead acid batteries as the modern LiFePO4 batteries can be monitored using voltage monitoring only. 

Current measurement using Hall element sensor

It tuned out that using a shunt makes a connection from the minus/ground of the 12 volt system into the Pi circuit. This not a good idea and can pose isolation problems (I have almost fried one laptop, blue smoke, due to positive gnd in a PC speaker set). Any connection between the high amp (100 A +) system and the low power Pi server computer system represent a hazard. Hence the Pi's 3.3 and 5 volt and associated ground or minus should be isolated. The output from the shunt connects to the operational amplifier which is not isolated from the power and gnd. A system with galvanic isolation between the 12 V circuit and the server power circuit (the Pi power) is essential. Applying the same mindset as if the 12 V system were a 220 VAC is sensible. A Hall-effect sensor to measure the current in a line fulfill the galvanic isolation request. The isolation is in the kV range, perfectly safe in all respect domestic and onboard.

The picture show a test setup using a 20 W 12 V light bulb as a load. The 12 volt circuit are switched using a DC Solid State Relay. The advantages over a mechanical relay are well known, the isolation from the switched ciruit and the control logic is in the kV range. Hence the microcomputer 3.3 and 5V ciruits are totally isolated from the high current 12V system. 

The Hall element current sensor runs on 5V and produce a voltage below and above the reference voltage it also emits, this reference voltage is just a stabilised voltage half the supply voltage. Hence the Analog to Digital converter is fed this differential signal and any positive number represent a current in one direction while a negative value represent a current in the oposite direction. 

A simple calibration using a multimeter was done and a small Python function could be written to measure the current flowing through the sensor. The function is given here:

def get_i():
    val=adc.read_adc_difference(0, gain=GAIN_0, data_rate=8)
    print("value ",val)
#  Large Hall element, http://www.yhdc.com/en/product/379/ 50A version.
#  Pins from edge Vref Vout 0V Vdd
#  Linear regression, cal using multimeter.
#  y (current) = -0.52372 + 0.0024236 * x (counts on ADC)
#    print(val,offset_0,sens)
    i = -0.52061 + 0.0024236 * val
    if abs(i)<0.1:  # Cheating, but avoiding non zero values at zero.
        i=0.0
    return i

Voltage measurement using a resitor ladder 

It's also important to measure the voltage on the 12V system. However, this also need to isolated from the computers circuit. As it's not possible to measure DC voltage with galvanic isolation without using very exotic devices I used a ladder of resistors to limit any leakage current. In the exampe I've used three resistors, 22k, 10k and 22k and feeding the voltage across the 10k resistor to the Analog to Digital converter. While not totally isolated the resitance pose a large restriction on any leakage current. In this setup the voltage can be extracted using a Python function like this:

def get_u():
# Note you can change the differential value to the following:
#  - 3 = Channel 2 minus channel 3
    val=adc.read_adc_difference(3, gain=GAIN_3, data_rate=8)
    val=val-offset_3 # make sure zero voltage is reported as zero.
    # 10k/54k=0.185185 ; rounded to 0.1841 after cal with voltmeter.
    u=(((val/32768)*4.096)/0.1841)
    if u<0.000001: # So close to zero that we flush to zero.
        u=0.0
    return u

The screendump at the right show what I real time X11 display might look like. With a fairly rapid update time it will display in real time both the voltage and current in the 12 V system. This could be in or out of the battery as in this battery monitor application or a more general power meter.  


A small demo test to read both voltage and current is show here:
#
# Simple demo to read voltage and current using ADS 1115
# in differential mode. 
import Adafruit_ADS1x15

# Instanciate a new ADS 1115 object
adc = Adafruit_ADS1x15.ADS1115()

# Set gain, possible values are:
#  - 2/3 = +/-6.144V
#  -   1 = +/-4.096V
#  -   2 = +/-2.048V
#  -   4 = +/-1.024V
#  -   8 = +/-0.512V
#  -  16 = +/-0.256V
GAIN_U = 4
GAIN_I = 1
# Note you can change the differential value to the following:
#  - 0 = Channel 0 minus channel 1
#  - 1 = Channel 0 minus channel 3
#  - 2 = Channel 1 minus channel 3
#  - 3 = Channel 2 minus channel 3
DIFF_U = 3
DIFF_I = 0

def get_u():
    # Get the voltage using differential measurement.
    # A resistor ladder +12---22k---10k---22k---GND
    # with measurement made over the 22k. This is to
    # partly isolate both +12V and 0V from the ADS and RPi.

    val=adc.read_adc_difference(DIFF_U, gain=GAIN_U, data_rate=8)
    # 10k/54k=0.185185 ; rounded to 0.1841 after cal with voltmetera.
    u=(((val/32768)*4.096)/0.1841)
    return u

def get_i():
    val=adc.read_adc_difference(DIFF_I, gain=GAIN_I, data_rate=8)
    #  Linear regression, cal using multimeter.
    #  y (current) = -0.52061 + 0.0024236 * x (counts)
    i = -0.52061 + 0.0024236 * val
    return i

u=get_u()
i=get_i()
print("Voltage: "+format(u,'6.2f')+" V\n"+"Current: "+\
       format(i,'6.2f')+" A\n")



Below is some earlier tests using a current shunt and measuring the voltage drop over it.

Early Current measurement using Tests using shunt

The shunt introduces a voltage drop of 75 mV at a certain current (5 A in my test setup, 200 A in a real case). The millivolt signal is somewhat small to be fed directly into the analog converter. The amplifier amplifies the voltage 50 times, which would bring up the 75 mV to 3.75 V a nice value for the analog  to digital converter. It happily accept voltages (in one range) from plus or minus 4.096 V. A maximum of 3.75 V is a quite good match, leaving a slight headroom for overload. The shunt can take some overload for short periods of time. 

Testing the amplifier, a small test was done to see if the amplifier was linear over  the range in question.The figure show a nice linear relationship between voltage drop over the shunt and voltage output  from the amplifier. The slope is about 50 which correspond good with the manufacturer's specification of an 50x amplification, the zero signal voltage should be as close to zero as possible. There is a trim possibility on the amplifier board. How much this zero voltage varies with temperature is unknown. The errors introduced however, are regarded as small and not really significant for this purpose.  The Julia code to make the plot is :

using Winston
x=[-29.6, -24.2, -22.9, -17.3, -11.1, 0, 11.3, 15.6, 20.7, 23.6, 27.4, 29.6]
y=[-1462., -1197., -1129., -857., -552., 0., 560., 768., 1019., 1163., 1349., 1462.] 
a,b=linreg(x,y)
p = FramedPlot(aspect_ratio=0.7,ylabel="Amp. output [mv]",xlabel="Shunt voltage [mV]",title="Voltage amplifier linearely test")
pt = Points(x,y,color="blue")
xreg=[minimum(x),maximum(x)]
yreg=[a+minimum(x)*b,a+maximum(x)*b]
rnl=Curve(xreg,yreg,color="red")
add(p,pt,rnl)
t=@sprintf("Intercept %5.2f, Slope %5.2f\n",a,b)
add(p, PlotLabel(.3, .8, t, color="red"))
plot(p)
savefig("Voltage-amplifier-lin.png")

Using the regression formula output = -1.36 + 49.36*(shunt voltage) it's easy to calculate the current.  Python code to calculate the output from the amplifer, which is the voltage converted to digital (the shunt is 75mV for 5A) :

y=float(input("Give voltage :"))          
a=-1.355789                               
b=49.362315     
x=(y-a)/b
amp=(((y-a)/b)/75)*5
print("Shunt voltage",x," mV")
print("Current :",amp," A")

The voltage is digitized using an ADS115 chip and the 16 bits will arrive as an integer from -32768 to 32768 using the scale set in the analog to digital converter, number of volt for full scale. With the scale set to plus/minus 4.096 volt then the value of 32768 correspond to 4.096 volt. Using the formula below full scale would represent a current of 5.5 A. The lowest current that can be detected is 4096/32768 volt which works to be 0.03 mV over the shunt and 0.03/75*5  is 2mA. Any load smaller than 2mA will not be detected by the battery monitor. A small current of 2mA will over a day amount to 48 mAh, a week 0.3 Ah. Barely detectable om a normal sized battery bank.

Testing and using the ADC (ADS1115 chip) as an ampere meter involves some math to calculate from a digital integer value to a current in Amperé.  The following program will display the current (and a few other data for reference) :

import time
import Adafruit_ADS1x15

adc = Adafruit_ADS1x15.ADS1115()
#  -   1 = +/-4.096V
#  -   2 = +/-2.048V

GAIN = 2
adc.start_adc(0, gain=GAIN, data_rate=8) # 8 samples per second, give more stable numbers then higher frequency. 

amp_zero=0.0  # Zero voltage, set with pot meter on millivolt amplifier
amp_gain=49.3623148 # Should be 50, but close enough.
adc_range=32768/(4096/GAIN)
full_range=5 # 75 mV at 5 Amp.
u_shunt=75   # 75 mV for 5 Amp.

offset=20  # Maybe the millivolt amplifier drift as it warms up.
while True:
    val=adc.get_last_result()
    val=val+offset
    u_amp=(val/adc_range+amp_zero)/1000
    i=(((val/adc_range+amp_zero)/amp_gain)/u_shunt)*full_range    
    print("ADC: ",val," Ampl volt: ",format(u_amp,'5.3f'),
          "V  Shunt volt: ",format(u_amp/amp_gain*1000,'5.3f'),
          "mV  Current ",format(i,'5.5f'),' A')
    time.sleep(10)



Comments