Skip to content

Instantly share code, notes, and snippets.

@cellularmitosis
Last active September 19, 2022 06:59
Show Gist options
  • Save cellularmitosis/7b20eb22d93b7383ea7adb42cca22cbf to your computer and use it in GitHub Desktop.
Save cellularmitosis/7b20eb22d93b7383ea7adb42cca22cbf to your computer and use it in GitHub Desktop.
Simple Arduino-based Li-ion battery 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment