Show by Label

Monday, June 13, 2016

_HowTo: Simple Method to Log Charging and Discharging of Li-Ion and Lipo cells

For my Raspberry Pi automatic power supply design with a UPS function (see another post), I wanted to learn more about the charging and discharging characteristics of Li-Ion and Lipo cells.

In several of my supply designs, I use the Adafruit Powerboost 1000c circuit. It uses an MCP73871 charge controller, that is set to deliver up to 1 Amp of charge current. It does not use a Thermistor to monitor the cell temperature, and I wanted to use a variation of cells.

The cells I tested were a TR 18650 with 2400mAh, a TR 14500 with 1200mAh, both are Li-Ion cells, and one small Lipo cell, model 043040 with 500mAh.

Before I modified the Powerboost circuit to limit the charge current to 500mA or a little less, I wanted to profile the charge/discharge characteristics of these three cells.

I used a RPi Model 3 powered by my automatic UPS supply, and I used another RPi, a Model (1) B, together with an A-2-D convertor circuit to monitor the current of the cells powering the Model 3.

The monitoring circuit is based on yet another post, and modified to measure the cell voltage together with the charging or discharging currents. Here is the circuit:

I used the following Python script to show the progress on the console and log the results into a file that I could upload to Excel to graph curves.

# Name:        MCP3002 Measure 5V
# Purpose:     Measure the voltage and current of a Li-Ion cell
# Author:      paulv
# Created:     22-10-2015, Modified 10-06-2016
# Copyright:   (c) paulv 2015, 2016
# Licence:     <your licence>

import spidev # import the SPI driver
from time import sleep, time, strftime
import subprocess
import sys
import os

# ==== constants
__author__ = 'Paul W. Versteeg'
VERSION = "1.0"

DEBUG = False
vref = 5.0 * 1000 # V-Ref in mV (Vref = VDD for the MCP3002)
resolution = 2**10 # for 10 bits of resolution
cal_CH0 = 0 # calibration in mV
cal_CH1 = 0
interval = 10       # interval between measurements in seconds
sample_interval = 2 # interval between samples in seconds
log_interval = interval - (2 * sample_interval) # log interval in seconds

# MCP3002 Control bits
#   7   6   5   4   3   2   1   0
#   X   1   S   O   M   X   X   X
# bit 6 = Start Bit
# S = SGL or \DIFF SGL = 1 = Single Channel, 0 = \DIFF is pseudo differential
# O = ODD or \SIGN
# in Single Ended Mode (SGL = 1)
#   ODD 0 = CH0 = + GND = - (read CH0)
#       1 = CH1 = + GND = - (read CH1)
# in Pseudo Diff Mode (SGL = 0)
#   ODD 0 = CH0 = IN+, CH1 = IN-
#       1 = CH0 = IN-, CH1 = IN+
# M = MSBF
# MSBF = 1 = LSB first format
#        0 = MSB first format
# ------------------------------------------------------------------------------

# SPI setup
spi_max_speed = 1000000 # 1 MHz (1.2MHz = max for 2V7 ref/supply)
# reason is that the ADC input cap needs time to get charged to the input level.
CE = 0 # CE0 | CE1, selection of the SPI device

spi = spidev.SpiDev(),CE) # Open up the communication to the device
spi.max_speed_hz = spi_max_speed

def read_mcp3002(channel):
    Function to read the data channels from an MCP300 A-2-D converter

    Control & Data Registers:
    See datasheet for more information
    send 8 bit control :
       X, Strt, SGL|!DIFF, ODD|!SIGN, MSBF, X, X, X
       0, 1,    1=SGL,     0 = CH0  , 0   , 0, 0, 0 = 96d
       0, 1,    1=SGL,     1 = CH1  , 0   , 0, 0, 0 = 112d

    receive 10 bit data :
       receive data range: 000..3FF (10 bits)
       MSB first: (set control bit in cmd for LSB first)
       spidata[0] =  X,  X,  X,  X,  X,  0, B9, B8
       spidata[1] = B7, B6, B5, B4, B3, B2, B1, B0
       LSB: mask all but B9 & B8, shift to left and add to the MSB


    if channel == 0:
        cmd = 0b01100000
        cmd = 0b01110000

    if DEBUG : print"cmd = ", cmd

    spi_data = spi.xfer2([cmd,0]) # send hi_byte, low_byte; receive hi_byte, low_byte

    if DEBUG : print("Raw ADC (hi-byte, low_byte) = {}".format(spi_data))

    adc_data = ((spi_data[0] & 3) << 8) + spi_data[1]
    return adc_data

def write_log(msg):
    Function to create a log of the readings with a time stamp.

    The fiels are seperated by a tab, so the file can be easily imported into
    an Excel spreadsheet to graph the results.

    The log results are appended, so the log file should be deleted for every


        dstamp = strftime("%d-%m-%Y")
        tstamp = strftime("%H:%M:%S")

        # open the log file and append results
        with open("/home/pi/lipo.log", "a") as fout:
            # Tabs are used to seperate the fields so technically it's not a real CSV format.
            # MS-Excel reads it anyway.
            fout.write (str(dstamp)+"\t"+(tstamp)+"\t"+str(msg)+"\n")

    except Exception as e:


def main():
        # create and setup the log file
        if not os.path.isfile('/home/pi/lipo.log'):
            # create the file and set the access mode
  ['touch /home/pi/lipo.log'], shell=True, \
                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  ['chmod goa+w /home/pi/lipo.log'], shell=True, \
                stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        write_log("\n===== Starting Log =====")

        print("Voltage and Current log from Li-Ion/Lipo cell")
        print("SPI max sampling speed = {}".format(spi_max_speed))
        print("V-Ref = {0} mV, Resolution = {1} bit".format(vref, resolution))
        print("SPI device = {0}".format(CE))

        while True:
            # average three readings to get a more stable result
            Vdata_1 = read_mcp3002(0) # get CH0 input (Volt)
            Cdata_1 = read_mcp3002(1) # get CH1 input (Current)
            Vdata_2 = read_mcp3002(0) # get CH0 input
            Cdata_2 = read_mcp3002(1) # get CH1 input
            Vdata_3 = read_mcp3002(0) # get CH0 input
            Cdata_3 = read_mcp3002(1) # get CH1 input

            Vdata = (Vdata_1+Vdata_2+Vdata_3)/3
            if DEBUG : print("V_Data (bin)    {0:010b}".format(Vdata))
            v_result = round((((Vdata * vref) / resolution)+ cal_CH0),2)
            if DEBUG : print ("V_result {} mV").format(v_result)
            voltage = round(v_result/1000.0,2) # convert to mV with 2 decimals
            print("Voltage  : {} V".format(voltage))

            Cdata = (Cdata_1+Cdata_2+Cdata_3)/3
            if DEBUG : print("C_Data (bin)    {0:010b}".format(Cdata))
            Vcurrent = round((((Cdata * vref) / resolution)+ cal_CH1),2)
            if DEBUG : print ("Vcurrent : {} mV").format(Vcurrent)
            current = int(Vcurrent/5) # convert to mA (gain 50, R = 0.1 Ohm)
            print("Current  : {} mA".format(current))
            if DEBUG : print("-----------------")

            # log the results
            msg = str(voltage) + "\t V\t" + str(current) + "\tmA"
            sleep(log_interval) # wait and loop back

    except KeyboardInterrupt: # Ctrl-C
        if DEBUG : print "Closing SPI channel"

if __name__ == '__main__':

Here is the charge curve for a tiny 043040 Lipo cell :

This is the one I use:

The "noise" that you see on the voltage and current graphs can be caused by the chemical process that takes place. I read somewhere that this is a "noisy" process.

This charge curve shows a charge current that is a little too high for comfort. The specification for the cell ( ) lists a maximum charge current of 1C, but a more safe value is 80% of that. The graph shows that there is a short period of time where the current is more than 630mA, so I needed to modify the Powerboost circuit to lower the maximum charge current.

This is quite simply done by changing one resistor value (R16, located just above the USB connection) from 1K0 to 2K2 (resulting in a maximum of 454mA), unfortunately it is an 0805 SMD component, so not that easy. Here is the schematic of the Powerboost :

Here is the charge graph with the limited charge current now topping at 454mA.

 And here the discharge curve for the 043040 cell:

The charge provides about 20 minutes of power to an Rpi-3 (in rest, idling!) at an average of about 450mA consumption current. The Powerboost circuit warns for a low-bat level, and my hardware removes the power from the RPi when that happens. The trip voltage is somewhere below 3.5-3.25V (it varies somewhat depending on the fall-off curve).

Here is the charge graph for a 14500 Li-Ion cell with the unmodified Powerboost:

This is the discharge graph for the 14500 cell:

And finally the charge graph for a Li-Ion 18650 with 2400mAh with the unmodified Powerboost :

And the 18650 discharge graph:

Note that it took 4 hours to charge this 2400mAh cell after it powered the Rpi-3 (idling!) for 3 hrs (at an average of  an 450mA consumption rate). Also notice the almost perfect linear discharge level. I read somewhere that they use these cells for the batteries in hybrid and electric cars like the Tesla.


If you like what you see, please support me by buying me a coffee:

No comments: