Blog 2022/7/21
<- previous | index | next ->
Initial Arduino sketch for a simple Li-ion capacity logger built with junk-bin parts.
See also https://www.eevblog.com/forum/projects/jellybean-li-ion-capacity-tester/
Blog 2022/7/21
<- previous | index | next ->
Initial Arduino sketch for a simple Li-ion capacity logger built with junk-bin parts.
See also https://www.eevblog.com/forum/projects/jellybean-li-ion-capacity-tester/
// Arduino Lithium-ion load / capacity tester. | |
// Copyright 2022 Jason Pepas. | |
// Released under the terms of the MIT License, see https://opensource.org/licenses/MIT | |
#include <stdint.h> | |
#include "types.h" | |
#define _READ_ADC_H_IMPLEMENTATION_ | |
#include "read_adc.h" | |
volts_t g_fullscale_v = 4.65f; | |
volts_t g_cutoff_v = 2.5f; // Terminate the test at this voltage. | |
// Pin assignments: | |
pin_t g_pwm_pin = 5; | |
pin_t g_rsense_pin = A0; | |
pin_t g_collector_pin = A10; | |
pin_t g_vcc_pin = A3; | |
pwm_t milliamps_to_pwm(milliamps_t milliamps) { | |
// PWM table: | |
// 10: 58 mA | |
// 20: 119 mA | |
// 30: 180 mA | |
// 40: 241 mA | |
if (milliamps <= 58) { | |
return milliamps / 58.0 * 10.0; | |
} else if (milliamps <= 119) { | |
return milliamps / 119.0 * 20.0; | |
} else if (milliamps <= 180) { | |
return milliamps / 180.0 * 30.0; | |
} else if (milliamps <= 241) { | |
return milliamps / 241.0 * 40.0; | |
// 50: 302 mA | |
// 60: 363 mA | |
// 70: 424 mA | |
// 80: 485 mA | |
// 90: 546 mA | |
} else if (milliamps <= 302) { | |
return milliamps / 302.0 * 50.0; | |
} else if (milliamps <= 363) { | |
return milliamps / 363.0 * 60.0; | |
} else if (milliamps <= 424) { | |
return milliamps / 424.0 * 70.0; | |
} else if (milliamps <= 485) { | |
return milliamps / 485.0 * 80.0; | |
} else if (milliamps <= 546) { | |
return milliamps / 546.0 * 90.0; | |
// 100: 607 mA | |
// 110: 668 mA | |
// 120: 728 mA | |
// 130: 789 mA | |
// 140: 849 mA | |
} else if (milliamps <= 607) { | |
return milliamps / 607.0 * 100.0; | |
} else if (milliamps <= 668) { | |
return milliamps / 668.0 * 110.0; | |
} else if (milliamps <= 728) { | |
return milliamps / 728.0 * 120.0; | |
} else if (milliamps <= 789) { | |
return milliamps / 789.0 * 130.0; | |
} else if (milliamps <= 849) { | |
return milliamps / 849.0 * 140.0; | |
// 150: 910 mA | |
// 160: 970 mA | |
// 170: 1030 mA | |
// 180: 1090 mA | |
// 190: 1150 mA | |
} else if (milliamps <= 910) { | |
return milliamps / 910.0 * 150.0; | |
} else if (milliamps <= 970) { | |
return milliamps / 970.0 * 160.0; | |
} else if (milliamps <= 1030) { | |
return milliamps / 1030.0 * 170.0; | |
} else if (milliamps <= 1090) { | |
return milliamps / 1090.0 * 180.0; | |
} else if (milliamps <= 1150) { | |
return milliamps / 1150.0 * 190.0; | |
// 200: 1211 mA | |
// 210: 1271 mA | |
// 220: 1329 mA | |
// 230: 1389 mA (oscillation) | |
// 240: 1445 mA (oscillation) | |
// 250: 1496 mA (oscillation) | |
} else if (milliamps <= 1211) { | |
return milliamps / 1211.0 * 200.0; | |
} else if (milliamps <= 1271) { | |
return milliamps / 1271.0 * 210.0; | |
} else if (milliamps <= 1329) { | |
return milliamps / 1329.0 * 220.0; | |
} else { | |
return 0; | |
} | |
} | |
void send_end_of_output() { | |
// send an empty line to signal "end-of-output". | |
Serial.print("\n"); | |
} | |
void log_battery_capacity(milliamps_t milliamps) { | |
uint32_t second = 0; | |
Serial.print("seconds,v_batt,v_rsense\n"); | |
analogWrite(g_pwm_pin, milliamps_to_pwm(milliamps)); | |
delay(100); | |
while (true) { | |
while (millis() % 1000 > 3) { | |
delay(1); | |
} | |
second += 1; | |
volts_t v_batt = read_adc_f(g_collector_pin, OVERSAMPLE_64x) / 1023.0f * g_fullscale_v; | |
volts_t v_rsense = read_adc_f(g_rsense_pin, OVERSAMPLE_64x) / 1023.0f * g_fullscale_v; | |
if (Serial.available() && Serial.read() == '.') { | |
analogWrite(g_pwm_pin, 0); | |
send_end_of_output(); | |
break; | |
} | |
Serial.print(second); | |
Serial.print(","); | |
Serial.print(v_batt, 3); | |
Serial.print(","); | |
Serial.print(v_rsense, 3); | |
Serial.print("\n"); | |
if (v_batt < g_cutoff_v) { | |
analogWrite(g_pwm_pin, 0); | |
send_end_of_output(); | |
break; | |
} | |
} | |
} | |
void setup() { | |
pinMode(g_pwm_pin, OUTPUT); | |
pinMode(g_rsense_pin, INPUT); | |
pinMode(g_collector_pin, INPUT); | |
pinMode(g_vcc_pin, INPUT); | |
Serial.begin(115200); | |
while (!Serial) { | |
; // Wait for serial port to connect. Needed for native USB port only. | |
} | |
Serial.print("\n"); | |
Serial.print("Li-ion capacity logger.\n"); | |
Serial.print("Single-character commands:\n"); | |
Serial.print("'0': start a test at 1000mA.\n"); | |
Serial.print("'6': start a test at 667mA.\n"); | |
Serial.print("'5': start a test at 500mA.\n"); | |
Serial.print("'3': start a test at 333mA.\n"); | |
Serial.print("'2': start a test at 250mA.\n"); | |
Serial.print("'1': start a test at 100mA.\n"); | |
Serial.print("'.': stop the test.\n"); | |
Serial.print("Empty line signals end-of-output.\n"); | |
} | |
void loop() { | |
if (Serial.available()) { | |
char ch = Serial.read(); | |
if (ch == '0') { | |
log_battery_capacity(1000); | |
} else if (ch == '6') { | |
log_battery_capacity(667); | |
} else if (ch == '5') { | |
log_battery_capacity(500); | |
} else if (ch == '3') { | |
log_battery_capacity(333); | |
} else if (ch == '2') { | |
log_battery_capacity(250); | |
} else if (ch == '1') { | |
log_battery_capacity(100); | |
} | |
} | |
} | |
void calibration_wip() { | |
// int ms = 8000; | |
// for (int i=0; i < 100; i += 10) { | |
// analogWrite(g_pwm_pin, i); delay(ms); | |
// } | |
// analogWrite(g_pwm_pin, 0); delay(ms); | |
// for (int i=100; i < 200; i += 10) { | |
// analogWrite(g_pwm_pin, i); delay(ms); | |
// } | |
// analogWrite(g_pwm_pin, 0); delay(ms); | |
// for (int i=200; i <= 255; i += 10) { | |
// analogWrite(g_pwm_pin, i); delay(ms); | |
// } | |
// int ms = 3000; | |
// analogWrite(g_pwm_pin, milliamps_to_pwm(0)); | |
// delay(ms); | |
// analogWrite(g_pwm_pin, milliamps_to_pwm(100)); // 95 mA | |
// delay(ms); | |
// analogWrite(g_pwm_pin, milliamps_to_pwm(250)); // 247 mA | |
// delay(ms); | |
// analogWrite(g_pwm_pin, milliamps_to_pwm(500)); // 496 mA | |
// delay(ms); | |
// analogWrite(g_pwm_pin, milliamps_to_pwm(750)); // 746 mA | |
// delay(ms); | |
// analogWrite(g_pwm_pin, milliamps_to_pwm(1000)); // 1001 mA | |
// delay(ms); | |
// analogWrite(g_pwm_pin, milliamps_to_pwm(1250)); // 1249 mA | |
// delay(ms); | |
} | |
// TODO: | |
// - use voltage divider to measure Vcc. | |
// - figure out why serial print causes pwm voltage to slightly dip |
#!/bin/bash | |
set -e -o pipefail | |
./logger.py /dev/tty.usbmodem* | tee log-$(date +%s).csv |
#!/usr/bin/env python | |
# logger.py: read CSV data from an Arduino. | |
# | |
# usage examples: | |
# ./logger.py /dev/ttyACM0 | tee log.txt | |
# ./logger.py /dev/tty.usbmodem14101 | tee log.txt | |
import serial | |
import sys | |
import signal | |
import time | |
def sigint_handler(signum, frame): | |
ser.write(".\n") | |
sys.exit(0) | |
if __name__ == "__main__": | |
if len(sys.argv) < 2: | |
sys.stdout.write("Usage: ./logger.py <device>\n") | |
sys.stdout.write("e.g. (Linux): ./logger.py /dev/ttyACM0 | tee log.txt\n") | |
sys.stdout.write("e.g. (macOS): ./logger.py /dev/tty.usbmodem14101 | tee log.txt\n") | |
sys.exit(1) | |
signal.signal(signal.SIGINT, sigint_handler) | |
# 115200, 8N1 | |
ser = serial.Serial( | |
port = sys.argv[1], # e.g. /dev/ttyACM0 or /dev/ttyUSB0 | |
baudrate = 115200, | |
bytesize=serial.EIGHTBITS, | |
stopbits = serial.STOPBITS_ONE, | |
parity = serial.PARITY_NONE, | |
timeout = 3 | |
) | |
# wait for the arduino to print the startup message. | |
time.sleep(0.1) | |
# discard the startup message. | |
ser.reset_input_buffer() | |
# send a '0' to start at 1000mA. | |
ser.write("5\n") | |
while True: | |
line = ser.readline() | |
if len(line.rstrip()) == 0: # empty line signals end-of-output. | |
break | |
else: | |
sys.stdout.write(line) | |
sys.stdout.flush() |
// Arduino ADC oversampling. | |
// Copyright 2021 Jason Pepas. | |
// Released under the terms of the MIT License, see https://opensource.org/licenses/MIT | |
#ifndef _READ_ADC_H | |
#define _READ_ADC_H | |
typedef int pin_t; // An Arduino pin number. | |
typedef int adc_t; // An Arduino 10-bit ADC value (0-1023). | |
typedef uint8_t oversample_t; // The degree of oversampling (e.g. 16x). | |
#define OVERSAMPLE_1x 1 | |
#define OVERSAMPLE_2x 2 | |
#define OVERSAMPLE_4x 4 | |
#define OVERSAMPLE_8x 8 | |
#define OVERSAMPLE_16x 16 | |
#define OVERSAMPLE_32x 32 | |
#define OVERSAMPLE_64x 64 | |
uint8_t shift_value_for(oversample_t oversample); | |
adc_t read_adc(pin_t sensor_pin, oversample_t oversample); | |
#endif | |
#ifdef _READ_ADC_H_IMPLEMENTATION_ | |
// Returns the shift value corresponding to the oversampling rate. | |
// Usage: accumulator >>= shift_value_for(OVERSAMPLE_16x); | |
uint8_t shift_value_for(oversample_t oversample) { | |
switch (oversample) { | |
case 1: return 0; | |
case 2: return 1; | |
case 4: return 2; | |
case 8: return 3; | |
case 16: return 4; | |
case 32: return 5; | |
case 64: return 6; | |
default: return 255; | |
} | |
} | |
// ADC read with oversampling. | |
// Usage: adc_t value = read_adc(my_sensor_pin, OVERSAMPLE_16x); | |
adc_t read_adc(pin_t sensor_pin, oversample_t oversample) { | |
// Note: arduino analogRead takes about 100us (10kHz) according to | |
// http://yaab-arduino.blogspot.com/2015/02/fast-sampling-from-analog-input.html | |
// An oversample rate of 64x fits 10-bit values neatly into a 16-bit unsigned accumulator, at about 150Hz. | |
uint16_t accumulator = 0; | |
for (uint8_t i=0; i < oversample; i++) { | |
int reading = analogRead(sensor_pin); | |
accumulator += reading; | |
} | |
accumulator >>= shift_value_for(oversample); | |
return (adc_t)accumulator; | |
} | |
// ADC read with oversampling, returning a float. | |
// Usage: float value = read_adc(my_sensor_pin, OVERSAMPLE_16x); | |
float read_adc_f(pin_t sensor_pin, oversample_t oversample) { | |
// Note: arduino analogRead takes about 100us (10kHz) according to | |
// http://yaab-arduino.blogspot.com/2015/02/fast-sampling-from-analog-input.html | |
// An oversample rate of 64x fits 10-bit values neatly into a 16-bit unsigned accumulator, at about 150Hz. | |
uint16_t accumulator = 0; | |
for (uint8_t i=0; i < oversample; i++) { | |
int reading = analogRead(sensor_pin); | |
accumulator += reading; | |
} | |
return accumulator / (float)oversample; | |
} | |
#endif |
// The Arduino preprocessor rearranges code, which sometimes moves function | |
// declarations above typedefs. If those functions rely on those types, | |
// this will cause a compilation error. | |
// The work-around is to move typedefs out into a header file, to ensure | |
// they are defined before the relocated function declarations. | |
#include <stdint.h> | |
typedef int pin_t; // an Arduino pin number. | |
typedef int adc_t; // An Arduino 10-bit ADC value (0-1023). | |
typedef int pwm_t; // an Arduino PWM output value (0-255). | |
typedef uint32_t millis_t; // Milliseconds as returned by millis(). | |
typedef int milliamps_t; // millamps. | |
typedef float volts_t; // Volts. |