Constant current battery capacity tester plans (Arduino logging)

piojo2

Newly Enlightened
Joined
Feb 20, 2018
Messages
7
I've been curious how to accurately measure battery capacity for some time, but I'm disinclined to buy a gadget I'd use so infrequently. The following is a simple circuit that draws a constant current and records the battery voltage every ten seconds. The data logging is handled by Arduino, which will log to your computer, and to an SD card writer if you have one. If you don't, you should remove "#define SD_LOGGING 1" at the top of the code.

Overview:
A voltage drop across a resistor is proportional to the current. An op amp can sense this voltage, compare it to a reference voltage, and tune its output so the feedback voltage matches the reference voltage. An Arduino senses the voltage directly from the battery, and turns off the current (via the MOSFET) when the battery voltage falls below a certain level. It is kept off until the Arduino is reset.

Components:
- The optional diode must be beefy enough to handle the current, and it should not drop too much voltage. One volt is fine. The diode, however, is optional. It keeps the battery from charging when the current flow is switched off. However, this is harmless, as the charge rate is miniscule (due to resistor R2 and the fact that op amps don't put out much current).
- The lithium ion battery needs to be held in a sled with thick wires and solid contact with the battery terminals. I actually used a clamp to hold the battery, and copper plates at each side.
- Instead of a switch and contact points for checking the current, you can temporarily remove the MOSFET and check the current with multimeter probes.
- Q1 is any Darlington transistor that can handle the current and heat dissipation. I suggest TIP-100, 101, or 102. It absolutely needs a heat sink, as it will dissipate nearly four watts, and a lot more if you modify the circuit to draw a higher current.
- The op amp should be a rail-to-rail model (MCP6002 from Microchip is very inexpensive), but it can also be a ground sensing op amp with an added diode or two after the output (not shown) to drop the output voltage.
- The resistors can be small, except for the 0.47 ohm resistor, which must dissipate half a watt.
- R1, the gate resistor, should be at least 200 ohms to protect the Arduino from the MOSFET's gate capacitance. The value is not important.
- Q2 is an N-channel power MOSFET with a low Rds(on) and logic-level input. I used IRLZ24N. Also popular: IRLZ44N, IRL520, or IRL540N. But there are hundreds of choices.
- The 3.3 V LDO regulator is interchangeable with various other parts. You can use a different voltage and different resistors, but the point is that you need to generate a 0.495 V reference voltage to use as an input to the op amp.
- To enable calibrating the circuit to get exactly an amp, replace one of the resistors with an adjustable resistor. (The greatest accuracy comes with using the smallest adjustable resistor you can, because they're usually not as accurate as a fixed resistor.)

Arduino connections:
- Vsense: pin A5
- control: pin A3
If you use the same SD card writer as I have (optional):
- chip select: pin 4
- mosi: pin 11
- miso: pin 12
- clk or sck: pin 13

The Arduino needs to be powered for the circuit to work, even though lights may come on when you connect the battery. Connect a battery and press the Reset button to start a test.

Calibration (Arduino):
Arduino can accurately measure voltage, but there's a constant in the code that needs to be tweaked differently for each Arduino. Or don't, as it'll still be pretty accurate. (In the code, see readVcc().)

Issues:
This circuit is a rough draft, and the current can't be changed much. An enhancement would allow choosing different currents by changing a jumper. However, the code might also need to be adjusted to cut the current flow sooner. When drawing just a few hundred milliamps, it would stress the battery to drain it all the way down to 3.1 V.

Due to contact resistance, especially on a breadboard, voltage measurements won't be perfectly accurate. Current will be constant, unless you jostle the wires and change the resistances. Soldering the board will give a much better result, and won't risk melting breadboard traces. If you use a breadboard, run as much current as you can through wires rather than the board. And don't send the current through crappy breadboard wires if you haven't made sure they have very low resistance.

Possible modifications:
- The current is determined by measuring the voltage drop across the load resistor. To get a higher current, use a higher reference voltage.
- A more professional solution (which doesn't care about contact resistance) is to use an instrumentation amplifier or current sense amplifier to measure the voltage drop across a shunt resistor, but shunts aren't cheap and you can't make one without nichrome.

Output:
The result, after you remove a couple lines of status from the beginning and end, is a CSV file, which can be plotted by various programs.

Arduino code:
/*
* SD card attached to SPI bus as follows:
** MOSI - pin 11
** MISO - pin 12
** CLK - pin 13
** CS - pin 4 (for MKRZero SD: SDCARD_SS_PIN)
*/

#include <SPI.h>
#include <SD.h>

#define SD_LOGGING 1

const int chipSelect = 4;

const int SWITCH_PIN = A3;
const int VOLTAGE_SENSE_PIN = A5;

String logFile;

void setup() {
pinMode(SWITCH_PIN, OUTPUT);
pinMode(VOLTAGE_SENSE_PIN, INPUT);

digitalWrite(SWITCH_PIN, LOW);

// Open serial communications and wait for port to open:
Serial.begin(9600);
//while (!Serial) {
// ; // wait for serial port to connect. Needed for native USB port only
//}

#if SD_LOGGING
Serial.print("Initializing SD card...");

// see if the card is present and can be initialized:
if (!SD.begin(chipSelect)) {
Serial.println("Card failed, or not present");
// don't do anything more:
while (1);
}
Serial.println("card initialized.");

int i = 0;
do
{
logFile = String("vlog_") + i + ".log";
++i;
}
while (SD.exists(logFile));
Serial.println("log file will be " + logFile);
#endif
}

void printVoltage(double voltage)
{
auto now = millis();
// open the file. note that only one file can be open at a time,
// so you have to close this one before opening another.
#if SD_LOGGING
File dataFile = SD.open(logFile, FILE_WRITE);
#endif

String dataString;
dataString += now;
dataString += ", ";

int length = 7;
char convertedVoltage[length+1];
// value, length, precision (in digits), char* buffer:
dtostrf(voltage, length, 5, convertedVoltage);
dataString += convertedVoltage;

Serial.println(dataString);

#if SD_LOGGING
// if the file is available, write to it:
if (dataFile) {
dataFile.println(dataString);
dataFile.close();
// print to the serial port too:
}
// if the file isn't open, pop up an error:
else {
Serial.print("Error opening ");
Serial.println(logFile);
}
#endif
}

double readVcc() {
// From https://hackingmajenkoblog.wordpress.com/2016/02/01/making-accurate-adc-readings-on-the-arduino/
long result;
// Read 1.1V reference against AVcc
ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
delay(2); // Wait for Vref to settle
ADCSRA |= _BV(ADSC); // Convert
while (bit_is_set(ADCSRA,ADSC));
result = ADCL;
result |= ADCH<<8;
// This constant must be calibrated per device:
result = 1093148L / result; // Back-calculate AVcc in mV
return result / 1000.0;
}

void loop() {
delay(1);

static unsigned long frameCount = -1ul;
++frameCount;
const int windowSize = 150;
static double voltages[windowSize];
double currentVoltage = analogRead(VOLTAGE_SENSE_PIN) / 1024.0 * readVcc();
voltages[frameCount % windowSize] = currentVoltage;
if (frameCount < windowSize-1)
return; // wait until we have a windor for average voltage

double averageVoltage = 0;
for (int i=0; i<windowSize; ++i)
averageVoltage += voltages;
averageVoltage /= windowSize;

static bool underVoltage = true;
static unsigned long storageTime;
if (underVoltage && averageVoltage > 4.1)
{
Serial.println(String("Enough voltage. Read ")+averageVoltage); // XXX
underVoltage = false;
// set up voltage recording
storageTime = millis();
digitalWrite(SWITCH_PIN, HIGH);
delay(100); // let the current stabilize
}
// TODO: 3.1 V cutoff is safe for C/2, but if I make lower discharge tests,
// this will need to change:
else if (!underVoltage && averageVoltage <= 3.1)
{
Serial.println(String("Under voltage. Read ")+averageVoltage); // XXX
underVoltage = true;
digitalWrite(SWITCH_PIN, LOW);
Serial.println("Shutting down."); // XXX
while (1)
;
}

if (!underVoltage && (millis() - storageTime) > 10000)
{
storageTime += 10000;
printVoltage(averageVoltage);
}
}

// vim: sw=2

a1nrQX1.jpg


uxGTuKz.jpg


pp_clamp.jpg

My battery holder is much like the above, but I used a smaller clamp. The pads are copper clad board with blobs of solder to make contact. Double sided tape helps hold them it place while adjusting.
 
Last edited:

piojo2

Newly Enlightened
Joined
Feb 20, 2018
Messages
7
I also really like this plan for a battery tester: https://www.edn.com/design/test-and-measurement/4368112/Circuit-measures-battery-capacity

But the EDN plan has a few problems:

  • Q1 should be a Darlington rather than a MOSFET. A power MOSFET may get fried when using voltage to control resistance (due to negative temperature coefficient of Vgs(th)). And if you use a Darlington, you need to drop a couple volts from the output so the op amp can drive its output to ground, or simply use a better op amp.
  • The voltage test points are correct for calibration, but in the wrong place for measuring battery voltage.
  • The other obvious point is that if it doesn't do data logging, you won't have the power curve, and you won't be able to find the records at all in a year.
 
Last edited:
Top