/*
* UltimateBatteryTester.ino
* Test internal resistance (ESR) of batteries and acquire and display the discharge graph
*
* To suspend a measurement while in storage mode, press single for storage of current capacity
* If measurement is stopped, it can be started by another press
* If pin 11 connected to ground, verbose output for Arduino Serial Monitor is enabled. This is not suitable for Arduino Plotter.
*
* Stored and displayed ESR is the average of the ESR's of the last storage period (1 min)
*
* The first 5 hours and 37 min, data is stored to EEPROM in a delta format. this are 337 + initial samples.
* When EEPROM space is exhausted, data is compressed, so another 5.6 hours fit into it.
* This allows to store 674 samples of voltage and milliampere and results in :
* 22h 24min at 2 minutes per storage
* 11h 12min at 1 minute per storage, for a Li-ion this is equivalent to around 3300 mAh
* 5h 36min at 30 seconds per storage
*
* One EEPROM block contains the initial voltage and current values as well as the capacity, battery type and value of the used load resistor.
* These values are stored at the beginning of the measurement
* The compressed values are stored as 4 bit deltas and written to EEPROM every second sample.
* The upper 4 bit store the first value, lower 4 bit store the second value.
* The capacity is stored at end of measurement or on button press during the storage.
*
*
* Copyright (C) 2021-2024 Armin Joachimsmeyer
* [email protected]
*
* https://github.com/ArminJo/Ultimate-Battery-Tester
*
* UltimateBatteryTester is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/gpl.html>.
*
*/
#include <Arduino.h>
#include "pitches.h"
/*
* Version 4.0 - 12/2023
* Use capacity between NominalFullVoltageMillivolt and SwitchOffVoltageMillivoltHigh as standard capacity to enable better comparison.
* If powered by USB plotter pin logic is reversed, i.e. plotter output is enabled if NOT connected to ground.
* In state detecting battery, you can toggle cutoff voltage between high, low and zero (0.1 V) with stop button.
* Fix bug for appending to compressed data.
* Synchronizing of LCD access for button handler, avoiding corrupted display content.
* Print improvements.
* Support for storage period of 120 s.
*
* Version 3.2.1 - 11/2023
* BUTTON_IS_ACTIVE_HIGH is not default any more
* Version 3.2 - 10/2023
* Cutoff LCD message improved
* Version 3.1 - 3/2023
* Fixed "conversion does not clear rest of EEPROM" bug
* Version 3.0 - 12/2022
* Improved compression
* Version 2.3 - 10/2022
* Increase no load settle time especially for NiMh batteries
* Attention tones
* Version 2.2 - 8/2022
* ESR > 64 bug fixed.
* Display of changes on pin PIN_DISCHARGE_TO_LOW
* Version 2.1 - 3/2022
* ESR is stored.
* Version 2.0 - 3/2022
* Improved version.
* Version 1.0 - 9/2021
* Tested version.
* Version 0.0 - 9/2021
* Initial version.
*/
#define VERSION_EXAMPLE "4.0"
//#define DEBUG
/*
* You should calibrate your ADC readout by replacing this value with the voltage you measured a the AREF pin after the program started.
* For my Nanos I measured e.g. 1060 mV and 1093 mV.
*/
#if !defined(ADC_INTERNAL_REFERENCE_MILLIVOLT)
#define ADC_INTERNAL_REFERENCE_MILLIVOLT 1100L // Change to value measured at the AREF pin. If value > real AREF voltage, measured values are > real values
#endif
/*
* Pin and ADC definitions
* Start/Stop button is connected to INT0 pin 2
* Pin 3 to 8 are used for parallel LCD connection
*/
#define ADC_CHANNEL_VOLTAGE 0 // ADC0 for voltage measurement
#define ADC_CHANNEL_CURRENT 1 // ADC1 for current measurement
#define PIN_VOLTAGE_RANGE_EXTENSION A2 // This pin is low to extend the voltage range from 2.2 volt to 4.4 volt
#define PIN_LOAD_HIGH A3 // This pin is high to switch on the high load (3 ohm)
// A4 + A5, the hardware I2C pins on Arduino, are used for Serial LCD
#define PIN_LOAD_LOW 12 // This pin is high to switch on the low load (10 ohm). A4 is occupied by I2C for serial LCD display.
#define PIN_TONE 9
// Mode pins
#define PIN_ONLY_PLOTTER_OUTPUT 10 // If powered by Li-ion, verbose output to Arduino Serial Monitor is disabled, if connected to ground. This is intended for Arduino Plotter mode.
// If powered by USB verbose verbose output to Arduino Serial Monitor is disabled, if NOT connected to ground.
#define PIN_DISCHARGE_TO_LOW 11 // If connected to ground, "cut off is low" is displayed and discharge ends at a lower voltage. E.g. Li-ion discharge ends at 3000 mV instead of 3500 mV
/*
* Imports and definitions for start/stop button at pin 2
*/
#define USE_BUTTON_0 // Enable code for button 0 at INT0 / pin 2.
#define NO_BUTTON_RELEASE_CALLBACK
//#define BUTTON_IS_ACTIVE_HIGH // If you have an active high button (sensor button) attached
#include "EasyButtonAtInt01.hpp"
void handleStartStopButtonPress(bool aButtonToggleState); // The button press callback function
EasyButton startStopButton0AtPin2(&handleStartStopButtonPress); // Button is connected to INT0 (pin2)
void checkForDelayedButtorProcessing(); // If LCD is in use do not process button
volatile bool sInLCDPrint; // To synchronize LCD access for button handler
bool sOnlyPlotterOutput; // contains the (inverted) value of the pin PIN_ONLY_PLOTTER_OUTPUT
#define DISCHARGE_MODE_TO_NORMAL 0 // is default case
#define DISCHARGE_MODE_TO_LOW 1
#define DISCHARGE_MODE_TO_ZERO 2 // End discharging at 0.1 volt (DISCHARGE_VOLTAGE_TO_ZERO_MILLIVOLT)
uint8_t sDischargeMode; // One of DISCHARGE_MODE_TO_NORMAL, DISCHARGE_MODE_TO_LOW and DISCHARGE_MODE_TO_ZERO. Starts with the (inverted) value of the pin PIN_DISCHARGE_TO_LOW
#define DISCHARGE_VOLTAGE_TO_ZERO_MILLIVOLT 50 // 50 mV
#define NO_BATTERY_MILLIVOLT (DISCHARGE_VOLTAGE_TO_ZERO_MILLIVOLT / 2) // 25 mV
bool sLastValueOfDischargeToLowPin; // To support changing between normal and low by using pin PIN_DISCHARGE_TO_LOW
/*
* External circuit definitions
*/
#define SHUNT_RESISTOR_MILLIOHM 2000L // 2 ohm
#define LOAD_LOW_MILLIOHM (1000 + SHUNT_RESISTOR_MILLIOHM) // Additional 1 ohm
#define LOAD_HIGH_MILLIOHM (10 * 1000 + SHUNT_RESISTOR_MILLIOHM) // Additional 10 ohm
#define ATTENUATION_FACTOR_VOLTAGE_LOW_RANGE 2L // Divider with 100 kOhm and 100 kOhm -> 2.2 V range
#define ATTENUATION_FACTOR_VOLTAGE_HIGH_RANGE 4L // Divider with 100 kOhm and 33.333 kOhm -> 4.4 V range
//#define NO_TONE_WARNING_FOR_VOLTAGE_TOO_LOW_FOR_STANDARD_CAPACITY_COMPUTATION
/*
* Activate the type of LCD you use
* Default is parallel LCD with 2 rows of 16 characters (1602).
* Serial LCD uses A4/A5 - the hardware I2C pins on Arduino
*/
#if !defined(USE_SERIAL_LCD) && !defined(USE_PARALLEL_LCD) && !defined(USE_NO_LCD)
#define USE_PARALLEL_LCD
#endif
//#define USE_SERIAL_LCD
// definitions for a 1602 LCD
#define LCD_COLUMNS 16
#define LCD_ROWS 2
#define LCD_MESSAGE_PERSIST_TIME_MILLIS 2000 // 2 second to view a message on LCD
#if defined(USE_SERIAL_LCD)
#include <LiquidCrystal_I2C.h> // Use an up to date library version which has the init method
#endif
#if defined(USE_PARALLEL_LCD)
#include "LiquidCrystal.h"
#endif
#if defined(USE_SERIAL_LCD) && defined(USE_PARALLEL_LCD)
#error Cannot use parallel and serial LCD simultaneously
#endif
#if defined(USE_SERIAL_LCD) || defined(USE_PARALLEL_LCD)
#define USE_LCD
#endif
#if defined(USE_SERIAL_LCD)
LiquidCrystal_I2C myLCD(0x27, LCD_COLUMNS, LCD_ROWS); // set the LCD address to 0x27 for a 16 chars and 2 line display
#endif
#if defined(USE_PARALLEL_LCD)
//LiquidCrystal myLCD(2, 3, 4, 5, 6, 7);
//LiquidCrystal myLCD(7, 8, A0, A1, A2, A3);
LiquidCrystal myLCD(7, 8, 3, 4, 5, 6);
#endif
/*
* Measurement timing
*/
#define MILLIS_IN_ONE_SECOND 1000L
#define SECONDS_IN_ONE_MINUTE 60L
//#define TEST // to speed up testing the code
#if defined(TEST)
#define STATE_INITIAL_ESR_DURATION_SECONDS 4 // 4 seconds (-2 for initial display of append message) before starting discharge and storing, to have time to just test for ESR of battery.
#define SAMPLE_PERIOD_OF_LOAD_ACIVATED_MILLIS 500 // The time of the activated load for one sample.
#define NUMBER_OF_SAMPLES_PER_STORAGE 5 // 1 minute, if we have 1 sample per second
#else
#define STATE_INITIAL_ESR_DURATION_SECONDS 32 // 32 seconds (-2 for initial display of append message) before starting discharge and storing, to have time to just test for ESR of battery.
#define SAMPLE_PERIOD_OF_LOAD_ACIVATED_MILLIS MILLIS_IN_ONE_SECOND // 1 s. The time of the activated load for one sample.
# if !defined(NUMBER_OF_SAMPLES_PER_STORAGE)
#define NUMBER_OF_SAMPLES_PER_STORAGE SECONDS_IN_ONE_MINUTE // 60, if we have 1 sample per second (SAMPLE_PERIOD_OF_LOAD_ACIVATED_MILLIS)
//#define NUMBER_OF_SAMPLES_PER_STORAGE (2 * SECONDS_IN_ONE_MINUTE) // 120, if we have 1 sample per second (SAMPLE_PERIOD_OF_LOAD_ACIVATED_MILLIS)
# endif
#endif
#define NUMBER_OF_SECONDS_PER_STORAGE NUMBER_OF_SAMPLES_PER_STORAGE // NUMBER_OF_SAMPLES_PER_STORAGE, if we have 1 sample per second
#define NUMBER_OF_STORAGES_PER_HOUR (3600L / NUMBER_OF_SECONDS_PER_STORAGE)
#define MAX_VALUES_DISPLAYED_IN_PLOTTER 500 // The Arduino 1.8 Plotter displays 500 values before scrolling
#define BATTERY_DETECTION_PERIOD_MILLIS (MILLIS_IN_ONE_SECOND / 2) // 500 ms
#define BATTERY_DETECTION_MINIMAL_MILLIVOLT 50
/*
* Values for different battery types
*/
struct BatteryTypeInfoStruct {
const char TypeName[11];
uint16_t DetectionThresholdVoltageMillivolt; // Type is detected if voltage is below this threshold
uint16_t NominalFullVoltageMillivolt; // The voltage to start the "standard" capacity computation
uint16_t SwitchOffVoltageMillivoltHigh; // The voltage to stop the "standard" capacity computation
uint16_t SwitchOffVoltageMillivoltLow;
uint8_t LoadType; // High (3 Ohm) or low (12 Ohm)
uint16_t LoadSwitchSettleTimeMillis; // Time for voltage to settle after load switch was disabled
};
#define LI_ION_MAX_FULL_VOLTAGE_MILLIVOLT 4300 // Maximum Voltage if fully loaded
#define LI_ION_STANDARD_FULL_VOLTAGE_MILLIVOLT 4100 // Start voltage for Li-ion standard capacity measurement
#define LI_ION_SWITCH_OFF_VOLTAGE_MILLIVOLT 3400 // Switch off voltage for Li-ion standard capacity measurement
#define LI_ION_SWITCH_OFF_VOLTAGE_MILLIVOLT_LOW 3000 // Switch off voltage for extended capacity measurement
#define NIMH_SWITCH_OFF_VOLTAGE_MILLIVOLT 1100 // Switch off voltage for NI-MH capacity measurement
#define NIMH_SWITCH_OFF_VOLTAGE_MILLIVOLT_LOW 1000 // Switch off voltage for extended capacity measurement
#define NO_LOAD 0
#define LOW_LOAD 1 // 12 ohm
#define HIGH_LOAD 2 // 3 ohm
#define TYPE_INDEX_NO_BATTERY 0
#define TYPE_INDEX_DEFAULT 6
#define TYPE_INDEX_MAX 10
struct BatteryTypeInfoStruct BatteryTypeInfoArray[] = { { "No battery", 100, 0, 0, 0, NO_LOAD, 0 }, /* Below 100 mV and not below 50, to avoid toggling between no and low batt */
{ "Low batt. ", 1000, 0, NIMH_SWITCH_OFF_VOLTAGE_MILLIVOLT_LOW, DISCHARGE_VOLTAGE_TO_ZERO_MILLIVOLT, HIGH_LOAD, 100 }, /* For researching of worn out batteries. */
{ "NiCd NiMH ", 1460, 1400, NIMH_SWITCH_OFF_VOLTAGE_MILLIVOLT, NIMH_SWITCH_OFF_VOLTAGE_MILLIVOLT_LOW, HIGH_LOAD, 100 }, /*400 mA*/
{ "Alkali ", 1550, 1500, 1300, 1000, HIGH_LOAD, 100 }, /*500 mA*/
{ "NiZn batt.", 1800, 1650, 1500, 1300, HIGH_LOAD, 100 }, /*550 mA*/
{ "LiFePO4 ", 3400, 3400, 3000, 2700, LOW_LOAD, 10 }, /*270 mA https://www.jackery.com/blogs/knowledge/ultimate-guide-to-lifepo4-voltage-chart*/
{ "Li-ion ", 5000, LI_ION_STANDARD_FULL_VOLTAGE_MILLIVOLT /*4100*/, LI_ION_SWITCH_OFF_VOLTAGE_MILLIVOLT/*3400*/,
LI_ION_SWITCH_OFF_VOLTAGE_MILLIVOLT_LOW/*3V*/,
LOW_LOAD, 10 }, /*300 mA*/
{ "LiIo 2pack", 2 * LI_ION_MAX_FULL_VOLTAGE_MILLIVOLT, 2 * LI_ION_STANDARD_FULL_VOLTAGE_MILLIVOLT, 2
* LI_ION_SWITCH_OFF_VOLTAGE_MILLIVOLT /*7V*/, 2 * LI_ION_SWITCH_OFF_VOLTAGE_MILLIVOLT_LOW /*6V*/,
LOW_LOAD, 10 }, /*620 mA*/
{ "9 V Block ", 9200, 9000, 7700, 7000, LOW_LOAD, 10 }, /*750 mA => external resistor recommended*/
{ "LiIo 3pack", 3 * LI_ION_MAX_FULL_VOLTAGE_MILLIVOLT, 3 * LI_ION_STANDARD_FULL_VOLTAGE_MILLIVOLT, 3
* LI_ION_SWITCH_OFF_VOLTAGE_MILLIVOLT, 3 * LI_ION_SWITCH_OFF_VOLTAGE_MILLIVOLT_LOW, LOW_LOAD, 10 }, /*925 mA*/
{ "LiIo 4pack", 4 * LI_ION_MAX_FULL_VOLTAGE_MILLIVOLT, 4 * LI_ION_STANDARD_FULL_VOLTAGE_MILLIVOLT, 4
* LI_ION_SWITCH_OFF_VOLTAGE_MILLIVOLT, 4 * LI_ION_SWITCH_OFF_VOLTAGE_MILLIVOLT_LOW, LOW_LOAD, 10 } /*1233 mA*/};
/*
* Current battery values set by getBatteryValues()
*/
struct BatteryInfoStruct {
uint16_t VoltageNoLoadMillivolt;
uint16_t VoltageLoadMillivolt;
int16_t Milliampere;
uint16_t Milliohm; // Average of last 60 values. ESR - Equivalent Series Resistor | internal battery resistance.
uint16_t sESRDeltaMillivolt = 0; // only displayed at initial ESR testing
uint32_t CapacityAccumulator;
uint16_t CapacityMilliampereHour;
uint16_t CapacityMilliampereHourStandardValueAtHighCutoff;
uint8_t LoadState; // NO_LOAD | LOW_LOAD 12 ohm | HIGH_LOAD 3 ohm
uint8_t TypeIndex;
} sBatteryInfo;
uint16_t sLastVoltageNoLoadMillivoltForBatteryCheck;
uint16_t sLastVoltageNoLoadMillivoltForPrint;
bool sBatteryWasInserted = false;
bool sBatteryWasDetectedAtLeastOnce = false;
/*
* Tester state machine
*/
#define STATE_SETUP_AND_READ_EEPROM 0
#define STATE_DETECTING_BATTERY 1 // Check if battery is inserted and determine type
#define STATE_INITIAL_ESR_MEASUREMENT 2 // Only voltage and ESR measurement every n seconds for STATE_INITIAL_ESR_DURATION_SECONDS seconds
#define STATE_STORE_TO_EEPROM 3 // Main measurement state, get values and store to EEPROM
#define STATE_STOPPED 4 // Switch off voltage reached, until removal of battery
volatile uint8_t sMeasurementState = STATE_SETUP_AND_READ_EEPROM;
// Override defaults defined in ADCUtils.h
#define LI_ION_VCC_UNDERVOLTAGE_THRESHOLD_MILLIVOLT 3500 // 3.5 volt
#define VCC_CHECK_PERIOD_MILLIS (60000L) // check every minute
#define VCC_UNDERVOLTAGE_CHECKS_BEFORE_STOP 5 // Shutdown after 5 times below VCC_UNDERVOLTAGE_THRESHOLD_MILLIVOLT or below VCC_EMERGENCY_UNDERVOLTAGE_THRESHOLD_MILLIVOLT
#include "ADCUtils.hpp"
/*
* Attention timing
*/
#define STATE_BATTERY_DETECTION_ATTENTION_PERIOD_MILLIS (MILLIS_IN_ONE_SECOND * 60)
#define STATE_STOP_ATTENTION_PERIOD_MILLIS (MILLIS_IN_ONE_SECOND * 600)
unsigned long sLastStateDetectingBatteryBeepMillis;
unsigned long sLastStateStoppedBeepMillis;
unsigned long sSampleCountForStoring;
unsigned long sLastMillisOfSample = 0;
unsigned long sLastMillisOfBatteryDetection = 0;
//unsigned long sLastMillisOfVCCCheck = VCC_CHECK_PERIOD_MILLIS; // to force first check at startup
unsigned long sFirstMillisOfESRCheck = 0;
uint16_t sVCCMillivolt;
/*
* Current value is in sCurrentLoadResistorHistory[0]. Used for computing and storing the average.
*/
#define HISTORY_SIZE_FOR_LOAD_RESISTOR_AVERAGE 16
uint16_t sCurrentLoadResistorHistory[HISTORY_SIZE_FOR_LOAD_RESISTOR_AVERAGE];
uint16_t sCurrentLoadResistorAverage;
/*
* Current value is in sESRHistory[0]. The average is stored in sBatteryInfo.Milliohm.
* For 9V and 60 mA we have a resolution of 0.3 ohm so we need an average.
*/
#define HISTORY_SIZE_FOR_ESR_AVERAGE 60
uint16_t sESRHistory[HISTORY_SIZE_FOR_ESR_AVERAGE];
/*
* EEPROM store
*/
#define FLAG_NO_COMPRESSION 0xFF
#define FLAG_COMPRESSION 0xFE
// The start values for the delta array
struct EEPROMStartValuesStruct {
uint16_t initialDischargingMillivolt;
uint16_t initialDischargingMilliampere;
uint16_t initialDischargingMilliohm;
uint8_t compressionFlag; // 0xFF -> not compressed, else compressed
uint16_t LoadResistorMilliohm;
uint8_t BatteryTypeIndex;
uint8_t DischargeMode;
uint16_t CapacityMilliampereHour; // is set at end of measurement or by store button
};
#if defined(TEST)
#define MAX_NUMBER_OF_SAMPLES 9
#else
// EEPROM size for values is (1024 - sizeof(EEPROMStartValues)) / 3 = 1012 / 3 = 337.3
#define MAX_NUMBER_OF_SAMPLES (E2END - sizeof(EEPROMStartValuesStruct)) / 3 // 337 + 1 since we always have the initial value. 5.6 h / 11.2 h for 1 minute sample rate
#endif
EEMEM int8_t sMillivoltDeltaArrayEEPROM[MAX_NUMBER_OF_SAMPLES];
EEMEM int8_t sMilliampereDeltaArrayEEPROM[MAX_NUMBER_OF_SAMPLES];
EEMEM int8_t sMilliohmDeltaArrayEEPROM[MAX_NUMBER_OF_SAMPLES];
EEMEM struct EEPROMStartValuesStruct EEPROMStartValues;
struct EEPROMStartValuesStruct StartValues;
/*
* Every compressed array byte contains two 4 bit values
* The upper 4 bit store the first value, the lower 4 bit store the second value
* 8 is added to the 4 bit integer (range from -8 and 7) to get positive values for storage
*/
struct ValuesForDeltaStorageStruct {
uint16_t lastStoredMilliampere;
uint16_t lastStoredVoltageNoLoadMillivolt;
uint16_t lastStoredMilliohm;
uint8_t tempMilliampereDelta;
uint8_t tempVoltageDelta;
uint8_t tempMilliohmDelta;
bool tempDeltaIsEmpty;
bool compressionIsActive;
int DeltaArrayIndex; // The index of the next values to be written. -1 to signal, that start values must be written.
} ValuesForDeltaStorage;
bool sDoPrintCaption = true; // Value used for (recursive) call to printValuesForPlotter().
uint8_t sVoltageDeltaArray[MAX_NUMBER_OF_SAMPLES]; // only used for readAndProcessEEPROMData(), but using local variable increases code size by 100 bytes
uint8_t sMilliampereDeltaArray[MAX_NUMBER_OF_SAMPLES];
uint8_t sMilliohmDeltaArray[MAX_NUMBER_OF_SAMPLES];
void getBatteryVoltageMillivolt();
bool detectAndPrintBatteryType();
void getBatteryCurrent();
void getBatteryValues();
bool checkStopCondition();
bool checkForBatteryRemoved();
void playEndTone();
void playAttentionTone();
void setLoad(uint8_t aNewLoadState);
void printStoredData();
void printVoltageNoLoadMillivolt();
void printBatteryValues();
void printValuesForPlotter(uint16_t aVoltageToPrint, uint16_t aMilliampereToPrint, uint16_t aMilliohmToPrint, bool aDoPrintCaption);
void printMillisValueAsFloat(uint16_t aValueInMillis);
void LCDPrintAsFloatWith2Decimals(uint16_t aValueInMillis);
void LCDPrintAsFloatWith3Decimals(uint16_t aValueInMillis);
void storeBatteryValuesToEEPROM(uint16_t aVoltageNoLoadMillivolt, uint16_t aMilliampere, uint16_t aMilliohm);
void storeCapacityAndDischargeModeToEEPROM();
void copyEEPROMDataToRam();
void readAndProcessEEPROMData(bool aDoConvertInsteadOfPrint);
void delayAndCheckForButtonPress();
void printButtonUsageMessage();
char getDischargeModeAsCharacter();
void printDischargeMode();
void printlnIfNotPlotterOutput();
void printStateString(uint8_t aState);
void switchToStateDetectingBattery();
void switchToStateInitialESRMeasurement();
void switchToStateStoreToEEPROM();
void switchToStateStopped(char aReasonCharacter);
void TogglePin(uint8_t aPinNr);
void LCDClearLine(uint8_t aLineNumber);
// Helper macro for getting a macro definition as string
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)
/*
* Program starts here
*/
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(PIN_LOAD_HIGH, OUTPUT);
pinMode(PIN_LOAD_LOW, OUTPUT);
pinMode(PIN_ONLY_PLOTTER_OUTPUT, INPUT_PULLUP);
pinMode(PIN_DISCHARGE_TO_LOW, INPUT_PULLUP);
setLoad(NO_LOAD);
digitalWrite(PIN_VOLTAGE_RANGE_EXTENSION, LOW); // prepare for later use
Serial.begin(115200);
#if defined(__AVR_ATmega32U4__) || defined(SERIAL_PORT_USBVIRTUAL) || defined(SERIAL_USB) /*stm32duino*/|| defined(USBCON) /*STM32_stm32*/|| defined(SERIALUSB_PID) || defined(ARDUINO_attiny3217)
delay(4000); // To be able to connect Serial monitor after reset or power up and before first print out. Do not wait for an attached Serial Monitor!
#endif
sOnlyPlotterOutput = !digitalRead(PIN_ONLY_PLOTTER_OUTPUT); // default behavior
// If powered by USB, PIN_ONLY_PLOTTER_OUTPUT logic is reversed. I.e. plotter output is enabled if NOT connected to ground.
bool tPoweredByUSB = isVCCUSBPowered();
if (tPoweredByUSB) {
sOnlyPlotterOutput = !sOnlyPlotterOutput; // reversed behavior if powered by USB
}
if (!sOnlyPlotterOutput) {
// Just to know which program is running on my Arduino
Serial.println(F("START " __FILE__ "\r\nVersion " VERSION_EXAMPLE " from " __DATE__));
/*
* Button pin info
*/
Serial.print(F("Button pin="));
Serial.println(INT0_PIN);
Serial.print(F("To suppress such prints not suited for Arduino plotter, "));
if (tPoweredByUSB) {
Serial.print(F("dis"));
}
Serial.print(F("connect pin " STR(PIN_ONLY_PLOTTER_OUTPUT) " "));
if (tPoweredByUSB) {
Serial.print(F("from"));
} else {
Serial.print(F("to"));
}
Serial.println(F(" ground"));
#if (NUMBER_OF_SAMPLES_PER_STORAGE * SAMPLE_PERIOD_OF_LOAD_ACIVATED_MILLIS != 60000)
Serial.print(F("Sample period="));
Serial.print(SAMPLE_PERIOD_OF_LOAD_ACIVATED_MILLIS);
Serial.print(F("ms, storage period="));
Serial.print(NUMBER_OF_SECONDS_PER_STORAGE);
Serial.println('s');
#endif
Serial.print(F("Maximum number of uncompressed samples="));
Serial.print(MAX_NUMBER_OF_SAMPLES + 1); // + 1 since we always have the initial value.
Serial.print(F(", compressed="));
Serial.print(MAX_NUMBER_OF_SAMPLES * 2); // The initial value is nor compressed.
Serial.print(F(" | "));
Serial.print(((MAX_NUMBER_OF_SAMPLES * 2) * (NUMBER_OF_SECONDS_PER_STORAGE / SECONDS_IN_ONE_MINUTE)) / 60);
Serial.print(F("h "));
Serial.print(((MAX_NUMBER_OF_SAMPLES * 2) * (NUMBER_OF_SECONDS_PER_STORAGE / SECONDS_IN_ONE_MINUTE)) % 60);
Serial.println(F("min"));
}
// Disable digital input on all unused ADC channel pins to reduce power consumption
DIDR0 = ADC0D | ADC1D;
#if defined(USE_LCD)
/*
* LCD initialization
*/
# if defined(USE_SERIAL_LCD)
myLCD.init();
myLCD.clear();
myLCD.backlight(); // Switch backlight LED on
# endif
# if defined(USE_PARALLEL_LCD)
myLCD.begin(LCD_COLUMNS, LCD_ROWS);
# endif
/*
* LCD print program, version and date
*/
myLCD.setCursor(0, 0);
myLCD.print(F("Battery Tester "));
myLCD.setCursor(0, 1);
myLCD.print(F(VERSION_EXAMPLE " " __DATE__));
delay(LCD_MESSAGE_PERSIST_TIME_MILLIS);
myLCD.setCursor(0, 1);
if (sOnlyPlotterOutput) {
myLCD.print(F("Only plotter out"));
} else {
myLCD.print(F("No plotter out "));
}
delay(LCD_MESSAGE_PERSIST_TIME_MILLIS);
#if (NUMBER_OF_SAMPLES_PER_STORAGE * SAMPLE_PERIOD_OF_LOAD_ACIVATED_MILLIS != 60000)
myLCD.setCursor(0, 1);
myLCD.print(NUMBER_OF_SECONDS_PER_STORAGE);
myLCD.print(F(" s / storage "));
delay(LCD_MESSAGE_PERSIST_TIME_MILLIS);
#endif
#endif
tone(PIN_TONE, 2200, 100); // usage of tone() costs 1524 bytes code space
/*
* Get and print EEPROM data
* get sDischargeMode for later appending
*/
readAndProcessEEPROMData(false);
printStoredData();
printlnIfNotPlotterOutput(); // end of stored data
/*
* Read value to variable in order to force printing triggered by value change :-)
*/
sLastValueOfDischargeToLowPin = digitalRead(PIN_DISCHARGE_TO_LOW);
/*
* If battery is still inserted, keep cutoff mode. I.e. measurement is likely to be continued.
* If battery was removed, cutoff mode can be chosen by pressing stop button.
*/
getBatteryVoltageMillivolt();
if (sBatteryInfo.VoltageNoLoadMillivolt < NO_BATTERY_MILLIVOLT) {
// Battery is removed here, so start with mode determined by pin
sDischargeMode = !sLastValueOfDischargeToLowPin;
StartValues.DischargeMode = sDischargeMode; // Required, in order to keep mode during conversion to compressed.
}
printDischargeMode(); // print actual discharge mode
if (sBatteryInfo.CapacityMilliampereHourStandardValueAtHighCutoff != 0) {
char tString[6];
sprintf_P(tString, PSTR("%4u"), sBatteryInfo.CapacityMilliampereHourStandardValueAtHighCutoff);
if (!sOnlyPlotterOutput) {
Serial.print(F("Standard capacity="));
Serial.print(tString);
Serial.println(F(" mAh"));
}
#if defined(USE_LCD)
myLCD.setCursor(9, 1);
myLCD.print(tString);
myLCD.print(F("mAh"));
#endif
}
switchToStateDetectingBattery();
}
/*
* The main loop with a delay of 100 ms
*/
void loop() {
if (sMeasurementState != STATE_STOPPED && isVCCUndervoltageMultipleTimes()) {
#if defined(USE_LCD)
myLCD.setCursor(0, 1);
myLCD.print(F("VCC undervoltage"));
myLCD.setCursor(7, 0);
myLCD.print(F("VCC="));
myLCD.print(getVCCVoltage(), 2);
myLCD.print('V');
#endif
playEndTone(); // 3 seconds
switchToStateStopped('U');
}
sOnlyPlotterOutput = !digitalRead(PIN_ONLY_PLOTTER_OUTPUT);
if (isVCCUSBPowered()) {
sOnlyPlotterOutput = !sOnlyPlotterOutput; // reversed behavior if powered by USB
}
if (sMeasurementState == STATE_DETECTING_BATTERY) {
if (millis() - sLastMillisOfBatteryDetection >= BATTERY_DETECTION_PERIOD_MILLIS) {
sLastMillisOfBatteryDetection = millis();
/*
* Check if battery was inserted
*/
if (detectAndPrintBatteryType()) {
// we waited 2 seconds in detectAndPrintBatteryType(), so must check if mode has not changed
if (sMeasurementState == STATE_DETECTING_BATTERY) {
switchToStateInitialESRMeasurement();
// If found, print button usage once at start of InitialESRMeasurement
if (!sBatteryWasDetectedAtLeastOnce) {
sBatteryWasDetectedAtLeastOnce = true;
printButtonUsageMessage();
}
// set load for the first call of getBatteryValues() to measure the current
setLoad(BatteryTypeInfoArray[sBatteryInfo.TypeIndex].LoadType);
}
} else {
// Not inserted, so print VCC voltage initially
#if defined(USE_LCD)
if (sMeasurementState == STATE_DETECTING_BATTERY) {
myLCD.setCursor(0, 0);
sVCCMillivolt = getVCCVoltageMillivolt();
myLCD.print(sVCCMillivolt / 1000.0, 2);
myLCD.print(F("V"));
}
#endif
/*
* if not connected to USB, check for attention every minute
*/
if (!isVCCUSBPowered() && millis() - sLastStateDetectingBatteryBeepMillis >= STATE_BATTERY_DETECTION_ATTENTION_PERIOD_MILLIS) {
sLastStateDetectingBatteryBeepMillis = millis();
playAttentionTone();
}
}
}
} else if ((unsigned) (millis() - sLastMillisOfSample)
>= (SAMPLE_PERIOD_OF_LOAD_ACIVATED_MILLIS + BatteryTypeInfoArray[sBatteryInfo.TypeIndex].LoadSwitchSettleTimeMillis)) {
sLastMillisOfSample = millis();
/*
* Do all this every second (of battery load)
*/
if (sMeasurementState == STATE_STOPPED) {
// Get battery no load voltage and print if changed
getBatteryVoltageMillivolt();
if (abs(sLastVoltageNoLoadMillivoltForPrint - sBatteryInfo.VoltageNoLoadMillivolt) > 5) {
printVoltageNoLoadMillivolt();
printlnIfNotPlotterOutput();
}
} else {
getBatteryValues(); // must be called only once per sample!
if (checkForBatteryRemoved()) {
switchToStateDetectingBattery(); // switch back to start
}
/*
* Always print battery values
* must be after battery removed detection!
*/
printBatteryValues();
}
/*
* Check for end of STATE_INITIAL_ESR_MEASUREMENT
*/
if (sMeasurementState == STATE_INITIAL_ESR_MEASUREMENT
&& millis() - sFirstMillisOfESRCheck >= (STATE_INITIAL_ESR_DURATION_SECONDS * MILLIS_IN_ONE_SECOND)) {
if (sBatteryInfo.VoltageNoLoadMillivolt < BatteryTypeInfoArray[sBatteryInfo.TypeIndex].NominalFullVoltageMillivolt) {
if (!sOnlyPlotterOutput) {
Serial.print(F("Start voltage "));
Serial.print(sBatteryInfo.VoltageNoLoadMillivolt);
Serial.print(F(" V is below NominalFullVoltageMillivolt of "));
Serial.print(BatteryTypeInfoArray[sBatteryInfo.TypeIndex].NominalFullVoltageMillivolt);
Serial.println(F(" V => standard capacity can not be computed!"));
}
#if defined(USE_LCD)
myLCD.setCursor(0, 0);
myLCD.print(F("Voltage too low "));
myLCD.setCursor(0, 1);
myLCD.print(F("for std capacity"));
#endif
#if !defined(NO_TONE_WARNING_FOR_VOLTAGE_TOO_LOW_FOR_STANDARD_CAPACITY_COMPUTATION)
for (uint_fast8_t i = 0; i < 3; ++i) {
delay(700);
tone(PIN_TONE, NOTE_C7, 200);
delay(400);
tone(PIN_TONE, NOTE_A6, 200);
}
#else
# if defined(USE_LCD)
delay(LCD_MESSAGE_PERSIST_TIME_MILLIS);
# endif
#endif
}
// If button was not pressed before, start a new data set
if (sMeasurementState == STATE_INITIAL_ESR_MEASUREMENT) {
// Force new data set
ValuesForDeltaStorage.DeltaArrayIndex = -1;
sBatteryInfo.CapacityAccumulator = 0;
memset(sCurrentLoadResistorHistory, 0, sizeof(sCurrentLoadResistorHistory)); // Clear history array
switchToStateStoreToEEPROM();
sSampleCountForStoring = NUMBER_OF_SAMPLES_PER_STORAGE; // store first value immediately
}
// end of state STATE_INITIAL_ESR_MEASUREMENT
}
if (sMeasurementState == STATE_STORE_TO_EEPROM) {
sSampleCountForStoring++;
/*
* Check for periodic storage to EEPROM
*/
if (sSampleCountForStoring >= NUMBER_OF_SAMPLES_PER_STORAGE) {
sSampleCountForStoring = 0;
storeBatteryValuesToEEPROM(sBatteryInfo.VoltageNoLoadMillivolt, sBatteryInfo.Milliampere, sBatteryInfo.Milliohm);
if (ValuesForDeltaStorage.DeltaArrayIndex == MAX_NUMBER_OF_SAMPLES && ValuesForDeltaStorage.compressionIsActive) {
/*
* Print message and stop, if compressed buffer is full
*/
if (!sOnlyPlotterOutput) {
Serial.println(F("EEPROM delta values array full -> stop measurement"));
}
switchToStateStopped('F');
} else if (ValuesForDeltaStorage.tempDeltaIsEmpty) {
/*
* Check for terminating condition, i.e. switch off voltage reached
*/
if (checkStopCondition()) {
switchToStateStopped('-');
#if defined(USE_LCD)
delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); // show "stopped"
myLCD.setCursor(7, 0);
myLCD.print(F(" Finished"));
#endif
// Play short melody
playEndTone();
}
}
}
}
if (sMeasurementState == STATE_STOPPED) {
/*
* Check for attention every 10 minute, after the current measurement was finished
*/
if (millis() - sLastStateStoppedBeepMillis >= STATE_STOP_ATTENTION_PERIOD_MILLIS) {
sLastStateStoppedBeepMillis = millis();
playAttentionTone();
}
}
}
delay(100);
/*
* Check for discharge mode pin change
*/
bool tValueOfDischargeToLowPin = digitalRead(PIN_DISCHARGE_TO_LOW);
if (sLastValueOfDischargeToLowPin != tValueOfDischargeToLowPin) {
sLastValueOfDischargeToLowPin = tValueOfDischargeToLowPin;
sDischargeMode = !sLastValueOfDischargeToLowPin;
// Update EEPROM value for right start value for next appending
if (StartValues.DischargeMode != sDischargeMode) {
StartValues.DischargeMode = sDischargeMode;
eeprom_update_byte(&EEPROMStartValues.DischargeMode, sDischargeMode);
}
printDischargeMode();
}
}
void printStateString(uint8_t aState) {
if (!sOnlyPlotterOutput) {
if (aState == STATE_DETECTING_BATTERY) {
Serial.print(F("DETECTING BATTERY"));
} else if (aState == STATE_INITIAL_ESR_MEASUREMENT) {
Serial.print(F("INITIAL ESR MEASUREMENT"));
} else if (aState == STATE_STORE_TO_EEPROM) {
Serial.print(F("STORE TO EEPROM"));
} else if (aState == STATE_STOPPED) {
Serial.print(F("STOPPED"));
}
}
}
void printlnIfNotPlotterOutput() {
if (!sOnlyPlotterOutput) {
Serial.println();
}
}
void printSwitchStateString() {
if (!sOnlyPlotterOutput) {
Serial.print(F("Switch to state "));
printStateString(sMeasurementState);
}
}
void switchToStateDetectingBattery() {
sMeasurementState = STATE_DETECTING_BATTERY;
printSwitchStateString();
printlnIfNotPlotterOutput();
sLastStateDetectingBatteryBeepMillis = millis();
sLastVoltageNoLoadMillivoltForBatteryCheck = 0XFFFF; // to force first check if voltage is 0
}
void switchToStateInitialESRMeasurement() {
sMeasurementState = STATE_INITIAL_ESR_MEASUREMENT;
printSwitchStateString();
printlnIfNotPlotterOutput();
sFirstMillisOfESRCheck = millis();
memset(sESRHistory, 0, sizeof(sESRHistory));
}
void switchToStateStoreToEEPROM() {
sMeasurementState = STATE_STORE_TO_EEPROM;
printSwitchStateString();
printlnIfNotPlotterOutput();
ValuesForDeltaStorage.tempDeltaIsEmpty = true; // required also for append to EEPROM, so do it here
}
/*
* aWriteToLCD default is true.
* @param aReasonCharacter, '-' for terminating condition met (regular end of measurement), U for VCC undervoltage, F for EEPROM full,
* D for button double press, B for button press.
*/
void switchToStateStopped(char aReasonCharacter) {
if (sMeasurementState != STATE_STOPPED) {
setLoad(NO_LOAD);
auto tOldMeasurementState = sMeasurementState;
sMeasurementState = STATE_STOPPED;
if (!sOnlyPlotterOutput) {
printSwitchStateString();
Serial.print(F(", reason="));
Serial.println(aReasonCharacter);
}
#if defined(USE_LCD)
myLCD.setCursor(0, 0);
myLCD.print(F("Stop measurement"));
delay(LCD_MESSAGE_PERSIST_TIME_MILLIS);
LCDClearLine(0);
#endif
if (tOldMeasurementState == STATE_STORE_TO_EEPROM) {
storeCapacityAndDischargeModeToEEPROM(); // Store capacity and discharge mode
}
#if defined(USE_LCD)
myLCD.setCursor(7, 0);
myLCD.print(F("Stopped "));
myLCD.print(aReasonCharacter);
#endif
sLastVoltageNoLoadMillivoltForPrint = 0; // to force display of NoLoad voltage
}
}
char getDischargeModeAsCharacter() {
if (sDischargeMode == DISCHARGE_MODE_TO_ZERO) {
return 'z';
} else if (sDischargeMode == DISCHARGE_MODE_TO_LOW) {
return 'l';
} else { // DISCHARGE_MODE_TO_NORMAL
return 'h';
}
}
void LCDPrintDischargeMode() {
#if defined(USE_LCD)
auto tDischargeMode = sDischargeMode;
myLCD.setCursor(0, 0);
if (tDischargeMode == DISCHARGE_MODE_TO_ZERO) {
myLCD.print(F("Cut off is 50 mV"));
} else {
if (sBatteryInfo.TypeIndex == TYPE_INDEX_NO_BATTERY) {
// Long text without voltage
myLCD.print(F("Cut off is "));
if (tDischargeMode == DISCHARGE_MODE_TO_LOW) {
myLCD.print(F("low "));
} else { // DISCHARGE_MODE_TO_NORMAL
myLCD.print(F("high "));
}
} else {
// Short text with voltage e.g. "Cutoff high 3.5V"
myLCD.print(F("Cutoff "));
uint16_t tSwitchOffVoltageMillivolt;
if (tDischargeMode == DISCHARGE_MODE_TO_LOW) {
myLCD.print(F("low "));
tSwitchOffVoltageMillivolt = BatteryTypeInfoArray[sBatteryInfo.TypeIndex].SwitchOffVoltageMillivoltLow;
} else { // DISCHARGE_MODE_TO_NORMAL
myLCD.print(F("high"));
tSwitchOffVoltageMillivolt = BatteryTypeInfoArray[sBatteryInfo.TypeIndex].SwitchOffVoltageMillivoltHigh;
}
if (tSwitchOffVoltageMillivolt < 10000) {
myLCD.print(' ');
}
myLCD.print(((float) tSwitchOffVoltageMillivolt) / 1000, 1);
myLCD.print('V');
}
}
delay(LCD_MESSAGE_PERSIST_TIME_MILLIS);
#endif
}
/*
* Prints state of discharge mode
*/
void printDischargeMode() {
auto tDischargeMode = sDischargeMode;
if (!sOnlyPlotterOutput) {
if (tDischargeMode == DISCHARGE_MODE_TO_ZERO) {
Serial.println(F("Discharge to 50 mV"));
} else {
if (sBatteryInfo.TypeIndex == TYPE_INDEX_NO_BATTERY) {
if (tDischargeMode == DISCHARGE_MODE_TO_LOW) {
Serial.println(F("Discharge to low voltage. e.g. 3000 mV for Li-ion"));
} else { // DISCHARGE_MODE_TO_NORMAL
Serial.println(F("Discharge to high voltage. e.g. 3450 mV for Li-ion"));
}
} else {
if (tDischargeMode == DISCHARGE_MODE_TO_LOW) {
Serial.print(F("Discharge to low voltage "));
Serial.print(BatteryTypeInfoArray[sBatteryInfo.TypeIndex].SwitchOffVoltageMillivoltLow);
} else { // DISCHARGE_MODE_TO_NORMAL
Serial.print(F("Discharge to high voltage "));
Serial.print(BatteryTypeInfoArray[sBatteryInfo.TypeIndex].SwitchOffVoltageMillivoltHigh);
}
Serial.println(F(" mV"));
}
}
}
LCDPrintDischargeMode();
}
/*
* Delay for LCD_MESSAGE_PERSIST_TIME_MILLIS but terminate if state was changed by button press
*/
void delayAndCheckForButtonPress() {
uint8_t tOldMeasurementState = sMeasurementState;
for (uint_fast8_t i = 0; i < 10; ++i) {
delay(LCD_MESSAGE_PERSIST_TIME_MILLIS / 10);
if (sMeasurementState != tOldMeasurementState) {
// Button press changes state here
break;
}
}
}
/*
* Print message "dbl press = stop" and "Press button to append to EEPROM"
* Wait if no button was pressed
*/
void printButtonUsageMessage() {
#if defined(USE_LCD)
uint8_t tOldMeasurementState = sMeasurementState;
#endif
if (!sOnlyPlotterOutput) {
Serial.println(F("Double press \"Start/stop\" button to stop measurement"));
}
#if defined(USE_LCD)
myLCD.setCursor(0, 0);
myLCD.print(F("dbl press = stop"));
/*
* and wait for 2 seconds for button press
*/
delayAndCheckForButtonPress();
if (sMeasurementState == tOldMeasurementState) {
#endif
if (!sOnlyPlotterOutput) {
Serial.println(F("Press \"Start/stop\" button to append values to already stored EEPROM data"));
}
#if defined(USE_LCD)
myLCD.setCursor(0, 0);
myLCD.print(F("Press button to "));
myLCD.setCursor(0, 1);
myLCD.print(F("append to EEPROM"));
/*
* and wait for 2 seconds for button press
*/
delayAndCheckForButtonPress();
}
#endif
}
void checkForDelayedButtorProcessing() {
// sInLCDPrint is false here, only if button handler was called after setting it to true
if (!sInLCDPrint) {
handleStartStopButtonPress(false); // delayed call to button handler
if (!sOnlyPlotterOutput) {
Serial.println(F("Call delayed button processing"));
}
} else {
sInLCDPrint = false; // Enable printing by button handler
}
}
/*
* Check for removed battery
* @return true if battery removed
*/
bool checkForBatteryRemoved() {
// check only if battery was inserted before
if (sBatteryWasInserted && sBatteryInfo.VoltageNoLoadMillivolt < NO_BATTERY_MILLIVOLT) {
if (!sOnlyPlotterOutput) {
Serial.println(F("Battery removing detected"));
}
sBatteryWasInserted = false;
return true;
}
return false;
}
/*
* !!!Called in ISR Context!!!
* !!! We can be called recursively, i.e. while waiting for 2 seconds we can be called for double press !!!
* Because interrupts are still enabled, millis() is working here :-)
*
* Ignore all presses in mode STATE_SETUP_AND_READ_EEPROM and STATE_DETECTING_BATTERY
* Double click in 2 seconds stop measurement. -> Goes to state STATE_STOPPED
*
* Single press:
* In mode STATE_STOPPED:
* Switch to state STATE_DETECTING_BATTERY
* In mode STATE_INITIAL_ESR_MEASUREMENT:
* Appends data to already stored ones in EEPROM -> switch to state STATE_STORE_TO_EEPROM
* In mode STATE_STORE_TO_EEPROM:
* Stores current capacity and stops measurement -> switch to state STATE_STOPPED
*/
void handleStartStopButtonPress(bool aButtonToggleState) {
(void) aButtonToggleState;
if (sMeasurementState == STATE_SETUP_AND_READ_EEPROM) {
if (!sOnlyPlotterOutput) {
Serial.println(F("Early press ignored"));
}
return;
}
// Do not process button as long as LCD is in use
if (sInLCDPrint) {
sInLCDPrint = false; // this forces a new call to handleStartStopButtonPress() at the end of PrintBatteryValues()
if (!sOnlyPlotterOutput) {
Serial.println(F("Press detected while LCD in use"));
}
return;
}
bool tIsDoublePress = startStopButton0AtPin2.checkForDoublePress( EASY_BUTTON_DOUBLE_PRESS_DEFAULT_MILLIS);
if (!sOnlyPlotterOutput) {
// Print should be done after checkForDoublePress() in order to not disturb the double press detection
Serial.print(F("Button pressed, state="));
printStateString(sMeasurementState);
Serial.println();
}
if (tIsDoublePress && sMeasurementState != STATE_DETECTING_BATTERY) {
/*
* Double press detected!
* Go to STATE_STOPPED
*/
if (!sOnlyPlotterOutput) {
Serial.println(F("Double press detected"));
}
switchToStateStopped('D');
} else {
/*
* Single press here
* Attention, this press can be the first press of a double press,
* so we must wait 2 seconds and check for double press before processing single press
*/
if (sMeasurementState == STATE_DETECTING_BATTERY) {
// Toggle discharge to normal, to low and to zero
sDischargeMode++;
if (sDischargeMode > DISCHARGE_MODE_TO_ZERO) {
sDischargeMode = DISCHARGE_MODE_TO_NORMAL;
}
printDischargeMode();
} else if (sMeasurementState == STATE_STORE_TO_EEPROM) {
switchToStateStopped('B'); // no check for double press required here :-)
} else {
/*
* Only state STATE_INITIAL_ESR_MEASUREMENT and STATE_STOPPED left here!
* Print start message, and wait for 2 seconds for double press detection
*/
uint8_t tOldMeasurementState = sMeasurementState;
#if defined(USE_LCD)
myLCD.setCursor(0, 0);
#endif
if (tOldMeasurementState == STATE_STOPPED) {
if (!sOnlyPlotterOutput) {
Serial.println(F("Start again"));
}
#if defined(USE_LCD)
myLCD.print(F("Start again "));
#endif
} else {
// STATE_INITIAL_ESR_MEASUREMENT here
if (!sOnlyPlotterOutput) {
Serial.println(F("Append data to EEPROM"));
}
#if defined(USE_LCD)
myLCD.print(F("Append to EEPROM"));
#endif
}
/*
* wait for double press detection, which means sMeasurementState can now also have value STATE_STOPPED here
*/
delayAndCheckForButtonPress();
// Must check old value (before possible double press) in order to avoid switching from STATE_STOPPED to DetectingBattery at each double press.
if (tOldMeasurementState == STATE_STOPPED) {
// start a new measurement cycle
switchToStateDetectingBattery();
} else if (sMeasurementState == STATE_INITIAL_ESR_MEASUREMENT) {
// No stop requested during 2 seconds wait -> append to EEPROM
switchToStateStoreToEEPROM();
/*
* Store next sample in 60 seconds, because we assume double press directly after entering state STATE_INITIAL_ESR_MEASUREMENT
* Otherwise we would start the appended data with a short sampling period.
*/
sSampleCountForStoring = 0;
}
}
#if defined(DEBUG)
if (!sOnlyPlotterOutput) {
Serial.print(F("New state="));
printStateString(sMeasurementState);
Serial.println();
}
#endif
}
}
void setLoad(uint8_t aNewLoadState) {
if (sBatteryInfo.LoadState != aNewLoadState) {
sBatteryInfo.LoadState = aNewLoadState;
#if defined(DEBUG)
Serial.print(F("Set load to "));
#endif
if (aNewLoadState == NO_LOAD) {
#if defined(DEBUG)
Serial.println(F("off"));
#endif
digitalWrite(PIN_LOAD_LOW, LOW); // disable 12 ohm load
digitalWrite(PIN_LOAD_HIGH, LOW); // disable 3 ohm load
} else if (aNewLoadState == LOW_LOAD) {
#if defined(DEBUG)
Serial.println(F("low"));
#endif
digitalWrite(PIN_LOAD_LOW, HIGH); // enable 12 ohm load
digitalWrite(PIN_LOAD_HIGH, LOW); // disable 3 ohm load
} else {
#if defined(DEBUG)
Serial.println(F("high"));
#endif
digitalWrite(PIN_LOAD_LOW, LOW); // disable 12 ohm load
digitalWrite(PIN_LOAD_HIGH, HIGH); // enable 3 ohm load
}
}
}
/*
* Sets VoltageNoLoadMillivolt or VoltageLoadMillivolt
* Provides automatic range switch between 2.2, 4.4 and 14 (up to 20 with 5V VCC) volt range
* The ranges are realized by a divider with 100 kOhm and 100 kOhm -> 2.2 V range and a divider with 100 kOhm and 33.333 kOhm -> 4.4 v range
* The 14 volt range is realized by using the 4.4 volt range with VCC (of at least 3.5 volt) as reference.
* With 5 volt VCC this range goes up to 20 volt.
* Does not affect the loads
*/
void getBatteryVoltageMillivolt() {
static bool sVoltageRangeIsLow = true;
uint16_t tInputVoltageRaw = waitAndReadADCChannelWithReference(ADC_CHANNEL_VOLTAGE, INTERNAL);
/*
* Automatic range
*/
if (sVoltageRangeIsLow && tInputVoltageRaw >= 0x3F0) {
// switch to higher voltage range by activating the range extension resistor at pin A2
sVoltageRangeIsLow = false;
pinMode(PIN_VOLTAGE_RANGE_EXTENSION, OUTPUT);
digitalWrite(PIN_VOLTAGE_RANGE_EXTENSION, LOW); // required???
#if defined(DEBUG)
if (!sOnlyPlotterOutput) {
Serial.println(F("Switch to 4.4 V range"));
}
#endif
tInputVoltageRaw = readADCChannelWithReference(ADC_CHANNEL_VOLTAGE, INTERNAL);
}
if (!sVoltageRangeIsLow) {
if (tInputVoltageRaw < (((0x3F0L * ATTENUATION_FACTOR_VOLTAGE_LOW_RANGE) / ATTENUATION_FACTOR_VOLTAGE_HIGH_RANGE) - 0x10)) {
// switch to lower voltage range by deactivating the range extension resistor at pin A2
sVoltageRangeIsLow = true;
pinMode(PIN_VOLTAGE_RANGE_EXTENSION, INPUT);
digitalWrite(PIN_VOLTAGE_RANGE_EXTENSION, LOW);
#if defined(DEBUG)
if (!sOnlyPlotterOutput) {
Serial.println(F("Switch to 2.2 V range"));
}
#endif
tInputVoltageRaw = readADCChannelWithReference(ADC_CHANNEL_VOLTAGE, INTERNAL);
} else if (tInputVoltageRaw >= 0x3F0) {
/*
* Here we have 17 mV resolution
* which leads to e.g. 0.3 ohm resolution at 9V and 60 mA
*/
#if defined(DEBUG)
if (!sOnlyPlotterOutput) {
Serial.print(F("Switch to "));
Serial.print(sVCCMillivolt / 1000.0, 3);
Serial.println(F(" V range"));
}
#endif
// switch to highest voltage range by using VCC as reference
uint16_t tReadoutFor1_1Reference = waitAndReadADCChannelWithReference(ADC_1_1_VOLT_CHANNEL_MUX, DEFAULT); // 225 at 5 volt VCC
tInputVoltageRaw = waitAndReadADCChannelWithReference(ADC_CHANNEL_VOLTAGE, DEFAULT);
#if defined(DEBUG)
Serial.print(tInputVoltageRaw);
Serial.print(F(" / "));
Serial.println(tReadoutFor1_1Reference);
#endif
// Adjust tInputVoltageRaw to a range above 1023 for computation of voltage below
tInputVoltageRaw = (tInputVoltageRaw * 1023L) / tReadoutFor1_1Reference;
}
}
#if defined(DEBUG)
Serial.print(F("tInputVoltageRaw="));
Serial.print(tInputVoltageRaw);
#endif
/*
* Compute voltage
*/
uint16_t tCurrentBatteryVoltageMillivolt;
if (sVoltageRangeIsLow) {
tCurrentBatteryVoltageMillivolt = (((ADC_INTERNAL_REFERENCE_MILLIVOLT * ATTENUATION_FACTOR_VOLTAGE_LOW_RANGE)
* tInputVoltageRaw) / 1023);
} else {
tCurrentBatteryVoltageMillivolt = (((ADC_INTERNAL_REFERENCE_MILLIVOLT * ATTENUATION_FACTOR_VOLTAGE_HIGH_RANGE)
* tInputVoltageRaw) / 1023);
}
if (sBatteryInfo.LoadState == NO_LOAD) {
sBatteryInfo.VoltageNoLoadMillivolt = tCurrentBatteryVoltageMillivolt;
} else {
sBatteryInfo.VoltageLoadMillivolt = tCurrentBatteryVoltageMillivolt;
}
#if defined(DEBUG)
Serial.print(F(" -> "));
Serial.print(tCurrentBatteryVoltageMillivolt);
Serial.println(F(" mV"));
#endif
}
/*
* Maximal current for a 2 ohm shunt resistor is 550 mA, and resolution is 0.54 mA.
*/
void getBatteryCurrent() {
uint16_t tShuntVoltageRaw = waitAndReadADCChannelWithReference(ADC_CHANNEL_CURRENT, INTERNAL);
sBatteryInfo.Milliampere =
(((ADC_INTERNAL_REFERENCE_MILLIVOLT * 1000L) * tShuntVoltageRaw) / (1023L * SHUNT_RESISTOR_MILLIOHM));
}
/*
* Assumes that load is activated before called
*/
void getBatteryValues() {
// Do it before deactivating the load
getBatteryCurrent();
getBatteryVoltageMillivolt(); // get current battery load voltage
// Deactivate load and wait for voltage to settle
// During the no load period switch on the LED
setLoad(NO_LOAD);
digitalWrite(LED_BUILTIN, HIGH);
delay(BatteryTypeInfoArray[sBatteryInfo.TypeIndex].LoadSwitchSettleTimeMillis);
getBatteryVoltageMillivolt(); // get current battery no load voltage
// restore original load state
setLoad(BatteryTypeInfoArray[sBatteryInfo.TypeIndex].LoadType);
digitalWrite(LED_BUILTIN, LOW);
sBatteryInfo.sESRDeltaMillivolt = sBatteryInfo.VoltageNoLoadMillivolt - sBatteryInfo.VoltageLoadMillivolt;
if (sBatteryInfo.Milliampere > 1) {
// New capacity computation
sBatteryInfo.CapacityAccumulator += sBatteryInfo.Milliampere;
sBatteryInfo.CapacityMilliampereHour = sBatteryInfo.CapacityAccumulator
/ ((3600L * MILLIS_IN_ONE_SECOND) / SAMPLE_PERIOD_OF_LOAD_ACIVATED_MILLIS); // = / 3600 for 1 s sample period
/*
* Compute sESRAverage
* Shift history array and insert current value
*/
uint8_t tESRAverageHistoryCounter = 1; // we always add sESRHistory[0]
uint32_t tESRAverageAccumulator = 0;
for (uint_fast8_t i = HISTORY_SIZE_FOR_ESR_AVERAGE - 1; i > 0; --i) {
if (sESRHistory[i - 1] != 0) {
// shift i-1 to i and add to average
#if defined(DEBUG)
Serial.print(sESRHistory[i - 1]);
Serial.print('+');
#endif
tESRAverageHistoryCounter++; // count only valid entries
tESRAverageAccumulator += sESRHistory[i - 1];
sESRHistory[i] = sESRHistory[i - 1];
}
}
// insert current value
uint32_t tESRMilliohm = (sBatteryInfo.sESRDeltaMillivolt * 1000L) / sBatteryInfo.Milliampere;
if (tESRMilliohm > __UINT16_MAX__) {
sESRHistory[0] = __UINT16_MAX__; // indicate overflow
} else {
sESRHistory[0] = tESRMilliohm;
}
tESRAverageAccumulator += sESRHistory[0];
sBatteryInfo.Milliohm = (tESRAverageAccumulator + (tESRAverageHistoryCounter / 2)) / tESRAverageHistoryCounter;
#if defined(DEBUG)
Serial.print(sESRHistory[0]);
Serial.print('/');
Serial.print(tESRAverageHistoryCounter);
Serial.print('=');
Serial.print(sBatteryInfo.Milliohm);
Serial.println();
#endif
/*
* Compute sCurrentLoadResistorAverage if array is full
* Formula is: LoadVoltage / LoadCurrent and includes the MosFet and the connector resistance.
* Shift load resistor history array and insert current value
*/
uint32_t tLoadResistorAverage = 0;
for (uint_fast8_t i = HISTORY_SIZE_FOR_LOAD_RESISTOR_AVERAGE - 1; i > 0; --i) {
tLoadResistorAverage += sCurrentLoadResistorHistory[i - 1];
sCurrentLoadResistorHistory[i] = sCurrentLoadResistorHistory[i - 1];
}
sCurrentLoadResistorHistory[0] = (sBatteryInfo.VoltageLoadMillivolt * 1000L / sBatteryInfo.Milliampere);
tLoadResistorAverage += sCurrentLoadResistorHistory[0];
if (sCurrentLoadResistorHistory[HISTORY_SIZE_FOR_LOAD_RESISTOR_AVERAGE - 1] != 0) {
/*
* as soon as array is filled up, compute rounded average load resistance value each time.
* Required for restoring battery capacity from stored data.
*/
sCurrentLoadResistorAverage = (tLoadResistorAverage + (HISTORY_SIZE_FOR_LOAD_RESISTOR_AVERAGE / 2))
/ HISTORY_SIZE_FOR_LOAD_RESISTOR_AVERAGE;
}
}
}
/*
* Play short melody
* Duration 3 seconds
*/
void playEndTone() {
tone(PIN_TONE, NOTE_A5);
delay(1000);
tone(PIN_TONE, NOTE_E5);
delay(1000);
tone(PIN_TONE, NOTE_A4, 1000);
delay(1000);
}
void playAttentionTone() {
tone(PIN_TONE, NOTE_C7, 40);
delay(100);
tone(PIN_TONE, NOTE_C7, 40);
delay(100);
}
/*
* Check for switch off voltage reached -> end of measurement
* @return true if stop condition met
*/
bool checkStopCondition() {
uint16_t tSwitchOffVoltageMillivolt;
if (sDischargeMode == DISCHARGE_MODE_TO_ZERO) {
tSwitchOffVoltageMillivolt = DISCHARGE_VOLTAGE_TO_ZERO_MILLIVOLT;
} else if (sDischargeMode == DISCHARGE_MODE_TO_LOW) {
tSwitchOffVoltageMillivolt = BatteryTypeInfoArray[sBatteryInfo.TypeIndex].SwitchOffVoltageMillivoltLow;
} else { // DISCHARGE_MODE_TO_NORMAL
tSwitchOffVoltageMillivolt = BatteryTypeInfoArray[sBatteryInfo.TypeIndex].SwitchOffVoltageMillivoltHigh;
}
if (sBatteryInfo.VoltageNoLoadMillivolt < tSwitchOffVoltageMillivolt) {
/*
* Switch off condition met
*/
setLoad(NO_LOAD);
if (!sOnlyPlotterOutput) {
Serial.print(F("Switch off voltage "));
Serial.print(tSwitchOffVoltageMillivolt);
Serial.print(F(" mV reached, capacity="));
Serial.print(sBatteryInfo.CapacityMilliampereHour);
Serial.println(F(" mAh"));
}
return true;
}
return false;
}
/*
* search the "database" for a matching type
*/
uint8_t getBatteryTypeIndex(uint16_t aBatteryVoltageMillivolt) {
// scan all threshold voltage of all battery types
for (uint_fast8_t i = 0; i < sizeof(BatteryTypeInfoArray) / sizeof(BatteryTypeInfoStruct) - 1; i++) {
if (aBatteryVoltageMillivolt < BatteryTypeInfoArray[i].DetectionThresholdVoltageMillivolt) {
#if defined(DEBUG)
Serial.print(F(" Battery index="));
Serial.print(i);
Serial.print(F(" BatteryVoltageMillivolt="));
Serial.print(aBatteryVoltageMillivolt);
Serial.print(F(" SwitchOffVoltageMillivolt="));
Serial.println(BatteryTypeInfoArray[i].SwitchOffVoltageMillivoltHigh);
#endif
return i;
}
}
// High voltage is detected
return sizeof(BatteryTypeInfoArray) / sizeof(BatteryTypeInfoStruct) - 1;
}
/*
* Disables the load, measures the voltage to detecting battery type and enables the load if battery detected
* @return true, if battery detected
*/
bool detectAndPrintBatteryType() {
setLoad(NO_LOAD);
getBatteryVoltageMillivolt();
if (sLastVoltageNoLoadMillivoltForBatteryCheck == sBatteryInfo.VoltageNoLoadMillivolt) {
sLastVoltageNoLoadMillivoltForBatteryCheck = sBatteryInfo.VoltageNoLoadMillivolt;
return false;
}
sBatteryInfo.TypeIndex = getBatteryTypeIndex(sBatteryInfo.VoltageNoLoadMillivolt);
if (sBatteryInfo.TypeIndex == TYPE_INDEX_NO_BATTERY) {
#if defined(USE_LCD)
myLCD.setCursor(5, 0);
myLCD.print(F(" "));
myLCD.print(F(" No batt."));
#endif
return false;
} else {
// print values
printVoltageNoLoadMillivolt();
if (!sOnlyPlotterOutput) {
Serial.print(F(" => "));
Serial.print(BatteryTypeInfoArray[sBatteryInfo.TypeIndex].TypeName);
Serial.println(F(" found"));
}
#if defined(USE_LCD)
myLCD.setCursor(0, 1);
myLCD.print(BatteryTypeInfoArray[sBatteryInfo.TypeIndex].TypeName);
myLCD.print(F(" found"));
// The current battery voltage is displayed, so clear "No batt." message selectively
myLCD.setCursor(7, 0);
myLCD.print(F(" "));
delay(LCD_MESSAGE_PERSIST_TIME_MILLIS);
LCDClearLine(1);
#endif
sBatteryWasInserted = true;
return true;
}
}
/*
* Called exclusively from setup() after readAndProcessEEPROMData()
*/
void printStoredData() {
#if defined(USE_LCD)
myLCD.setCursor(0, 0);
myLCD.print(getVCCVoltage(), 1);
myLCD.print(F("V Stored data"));
#endif
/*
* Print battery values, and use state STATE_SETUP_AND_READ_EEPROM for formatting
* "0.061o l 1200mAh" using sBatteryInfo.Milliohm
*/
printBatteryValues();
#if defined(USE_LCD)
delay(LCD_MESSAGE_PERSIST_TIME_MILLIS);
#endif
}
void printVoltageNoLoadMillivolt() {
uint16_t tVoltageNoLoadMillivolt = sBatteryInfo.VoltageNoLoadMillivolt; // saves 12 bytes programming space
if (!sOnlyPlotterOutput) {
sLastVoltageNoLoadMillivoltForPrint = tVoltageNoLoadMillivolt;
printMillisValueAsFloat(tVoltageNoLoadMillivolt);
Serial.print(F(" V"));
}
#if defined(USE_LCD)
myLCD.setCursor(0, 0);
LCDPrintAsFloatWith3Decimals(tVoltageNoLoadMillivolt);
myLCD.print(F("V "));
// cursor is now at 7, 0
#endif
}
/*
* Evaluates sMeasurementState and prints:
* - sBatteryInfo.VoltageNoLoadMillivolt
* - sBatteryInfo.Milliampere
* - sBatteryInfo.Milliohm
* - optional sESRDeltaMillivolt or capacity
* to Serial and LCD
*
* STATE_INITIAL_ESR_MEASUREMENT:
* "4.030 V, 27 s 329 mA at 11.896 ohm, ESR=0.329 ohm, 0.108 V"
* 0 4 8 C F
* "4.030V 18 329mA" printing down counter
* "0.061o l 0.128V" using current ESR from sESRHistory[0]
*
* STATE_STORE_TO_EEPROM:
* "4.030 V, 329 mA at 11.949 ohm, ESR=0.329 ohm, capacity=1200 mAh
* 0 4 8 C F
* "4.030V 312 329mA" printing EEPROM array index
* "0.392o l 1200mAh" using sBatteryInfo.Milliohm
*
* Called with states STATE_SETUP_AND_READ_EEPROM, STATE_INITIAL_ESR_MEASUREMENT and STATE_STORE_TO_EEPROM
*/
void printBatteryValues() {
char tString[6];
sInLCDPrint = true; // disable printing by button handler
uint8_t tMeasurementState = sMeasurementState; // Because sMeasurementState is volatile
if (tMeasurementState != STATE_SETUP_AND_READ_EEPROM) {
/***********************************************************************************
* First row only for state STATE_INITIAL_ESR_MEASUREMENT or STATE_STORE_TO_EEPROM
***********************************************************************************/
/*
*/
printVoltageNoLoadMillivolt();
if (!sOnlyPlotterOutput) {
// 4.094 V, 334 mA at 11.949 ohm, ESR=0.334 ohm, capacity=3501 mAh
Serial.print(F(", "));
}
if (tMeasurementState == STATE_INITIAL_ESR_MEASUREMENT) {
/*
* Print down counter for STATE_INITIAL_ESR_MEASUREMENT
*/
uint8_t tSecondsToGo = STATE_INITIAL_ESR_DURATION_SECONDS - ((millis() - sFirstMillisOfESRCheck) / 1000);
if (!sOnlyPlotterOutput) {
Serial.print(tSecondsToGo);
Serial.print(F(" s, ")); // seconds until discharging
}
if (tSecondsToGo < 10) {
#if defined(USE_LCD)
myLCD.print(' '); // padding space for count
#endif
tone(PIN_TONE, 2000, 40); // costs 1524 bytes code space
}
#if defined(USE_LCD)
myLCD.print(tSecondsToGo);
myLCD.print(' '); // trailing space for count (just in case mA are > 999)
#endif
} else {
/*
* Print index counter for STATE_STORE_TO_EEPROM
*/
#if defined(USE_LCD)
int tDeltaArrayIndex = ValuesForDeltaStorage.DeltaArrayIndex; // saves 8 bytes of program space
if (ValuesForDeltaStorage.compressionIsActive) {
tDeltaArrayIndex *= 2;
}
// We start with array index -1, which indicates initialization of array :-)
if (tDeltaArrayIndex < 10 && tDeltaArrayIndex >= 0) {
myLCD.print(' '); // we have "-1" once, because we store values (and increment index) after print
}
if (tDeltaArrayIndex < 100) {
myLCD.print(' '); // padding space :-)
}
myLCD.print(tDeltaArrayIndex);
#endif
}
// cursor is now at 9, 0
/*
* Print milliampere
*/
sprintf_P(tString, PSTR("%4u"), sBatteryInfo.Milliampere);
if (!sOnlyPlotterOutput) {
Serial.print(tString);
Serial.print(F(" mA at "));
printMillisValueAsFloat(sCurrentLoadResistorHistory[0]);
Serial.print(F(" ohm, "));
}
#if defined(USE_LCD)
myLCD.print(tString);
myLCD.print(F("mA"));
#endif
}
/**********************
* Start of second row
**********************/
/*
* STATE_SETUP_AND_READ_EEPROM + STATE_STORE_TO_EEPROM: "0.061o l 1200mAh" using sBatteryInfo.Milliohm
* STATE_INITIAL_ESR_MEASUREMENT: "0.061o l 0.128V" using current ESR from sESRHistory[0]
*/
uint32_t tMilliohm;
if (tMeasurementState == STATE_INITIAL_ESR_MEASUREMENT) {
tMilliohm = sESRHistory[0];
} else {
tMilliohm = sBatteryInfo.Milliohm;
}
if (!sOnlyPlotterOutput) {
Serial.print(F("ESR="));
if (tMilliohm == __UINT16_MAX__ || sBatteryInfo.Milliampere == 0) {
Serial.print(F("overflow, "));
} else {
printMillisValueAsFloat(tMilliohm);
Serial.print(F(" ohm, "));
}
}
#if defined(USE_LCD)
myLCD.setCursor(0, 1);
if (tMilliohm == __UINT16_MAX__ || sBatteryInfo.Milliampere == 0) {
myLCD.print(F("99.99")); // Overflow
} else if (tMilliohm < 10000) {
myLCD.print(((float) (tMilliohm)) / 1000, 3);
} else {
myLCD.print(((float) (tMilliohm)) / 1000, 2);
}
myLCD.print(F("\xF4 ")); // Ohm symbol
myLCD.setCursor(7, 1); // This avoids problems with values >= 10 ohm
myLCD.print(getDischargeModeAsCharacter());
myLCD.print(' ');
#endif
/*
* Print voltage difference or capacity
*/
if (tMeasurementState == STATE_INITIAL_ESR_MEASUREMENT) {
/*
* Print voltage difference at load
*/
uint16_t tESRDeltaMillivolt = sBatteryInfo.sESRDeltaMillivolt; // saves 4 bytes programming space
if (!sOnlyPlotterOutput) {
printMillisValueAsFloat(tESRDeltaMillivolt);
Serial.print(F(" V "));
}
#if defined(USE_LCD)
myLCD.print(' '); // leading space only for voltage
LCDPrintAsFloatWith3Decimals(tESRDeltaMillivolt);
myLCD.print(F("V"));
#endif
} else {
/*
* Print capacity
*/
sprintf_P(tString, PSTR("%4u"), sBatteryInfo.CapacityMilliampereHour);
if (!sOnlyPlotterOutput) {
Serial.print(F("capacity="));
Serial.print(tString);
Serial.print(F(" mAh"));
}
#if defined(USE_LCD)
myLCD.print(tString);
myLCD.print(F("mAh"));
#endif
}
printlnIfNotPlotterOutput();
checkForDelayedButtorProcessing();
}
void printMillisValueAsFloat(uint16_t aValueInMillis) {
Serial.print(((float) (aValueInMillis)) / 1000, 3);
}
void LCDPrintAsFloatWith3Decimals(uint16_t aValueInMillis) {
#if defined(USE_LCD)
myLCD.print(((float) (aValueInMillis)) / 1000, 3);
#else
(void) aValueInMillis;
#endif
}
void LCDPrintAsFloatWith2Decimals(uint16_t aValueInMillis) {
#if defined(USE_LCD)
myLCD.print(((float) (aValueInMillis)) / 1000, 2);
#else
(void) aValueInMillis;
#endif
}
/*
* Just clear the complete EEPROM
*/
void clearEEPROMTo_FF() {
if (!sOnlyPlotterOutput) {
Serial.println(F("Clear EEPROM"));
}
for (int i = 0; i < E2END; ++i) {
eeprom_update_byte((uint8_t*) i, 0xFF);
}
}
/*
* compressed delta
* upper 4 bit store the first value (between -8 and 7), lower 4 bit store the second value
* 7 / F is interpreted as 28 enabling values of 22 (28 -6) to 33 (28 +5) in 2 steps
* 6 / E is interpreted as 16 enabling values of 10 (16 -6) to 21 (16 +5) in 2 steps
* 5 / D is interpreted as 5
* 4 / C
* 2 / A
* 0 / 8
* -2 / 6
* -4 / 4
* -6 / 2 is interpreted as -6
* -7 / 1 is interpreted as -18 enabling values of -13 (-18 +5) to -24 (-18 -6) in 2 steps
* -8 / 0 is interpreted as -30 enabling values of -25 (-30 +5) to -36 (-30 -6) in 2 steps
* @param aDelta The delta to process
* @param *aDeltaTemp Storage of the upper 4 bit delta, which cannot directly be written to EEPROM
* @return clipped aDelta | aDelta which is stored
*/
int16_t storeDeltas(int16_t aDelta, uint8_t *aDeltaTemp, uint8_t *aEEPROMAddressToStoreValue) {
if (!sOnlyPlotterOutput) {
Serial.print(' ');
Serial.print(aDelta);
}
if (!ValuesForDeltaStorage.compressionIsActive) {
// No compression, only clip to 8 bit range
if (aDelta > __INT8_MAX__) {
aDelta = __INT8_MAX__;
} else if (aDelta < -128) {
aDelta = -128;
}
int8_t tDelta = aDelta;
eeprom_write_byte(aEEPROMAddressToStoreValue, tDelta);
return aDelta;
}
/*
* Compression here. Clip aDelta to the available range
*/
int8_t tDelta;
if (aDelta >= 22) {
aDelta = 28;
tDelta = 7;
} else if (aDelta >= 10) {
aDelta = 16;
tDelta = 6;
} else if (aDelta >= 5) {
aDelta = 5;
tDelta = 5;
} else if (aDelta <= -25) {
aDelta = -30;
tDelta = -8; // -> 0
} else if (aDelta <= -13) {
aDelta = -18;
tDelta = -7; // -> 1
} else if (aDelta <= -6) {
aDelta = -6;
tDelta = -6; // -> 2
} else {
// Here values from -6 to 5
tDelta = aDelta;
}
/*
* convert delta to an unsigned value by adding 8 => -8 to 7 -> 0 to F
* F is +7, 8 is 0, 0 is -8 tDelta is positive now :-)
*/
tDelta += 8;
uint8_t tDeltaToStore;
if (ValuesForDeltaStorage.tempDeltaIsEmpty) {
tDeltaToStore = tDelta << 4; // Store in upper 4 bit
*aDeltaTemp = tDeltaToStore;
} else {
// upper 4 bit store the first value (between -8 and 7), lower 4 bit store the second value
tDeltaToStore = *aDeltaTemp | tDelta;
eeprom_write_byte(aEEPROMAddressToStoreValue, tDeltaToStore);
if (tDeltaToStore != eeprom_read_byte(aEEPROMAddressToStoreValue)) {
// Yes, I have seen this (starting with index 4 6 times 0xFF for current). Maybe undervoltage while powered by battery.
tone(PIN_TONE, NOTE_C7, 20);
delay(40);
tone(PIN_TONE, NOTE_C6, 20);
delay(40);
tone(PIN_TONE, NOTE_C7, 20);
}
}
if (!sOnlyPlotterOutput) {
Serial.print(F("->0x"));
Serial.print(tDeltaToStore, HEX);
}
return aDelta;
}
/*
* Store values to EEPROM as 4 bit deltas between sBatteryInfo and ValuesForDeltaStorage and write them to EEPROM every second call
* Upper 4 bit store the first value, lower 4 bit store the second value
*/
void storeBatteryValuesToEEPROM(uint16_t aVoltageNoLoadMillivolt, uint16_t aMilliampere, uint16_t aMilliohm) {
if (ValuesForDeltaStorage.DeltaArrayIndex < 0) {
if (!sOnlyPlotterOutput) {
Serial.println(F("Store initial values to EEPROM"));
}
clearEEPROMTo_FF(); // this may last one or two seconds
/*
* Initial values
* Storing them in a local structure and storing this, costs 50 bytes code size
* Storing them in a global structure and storing this, costs 30 bytes code size
*
* Initially set up structure for start values, also used for printing
* and store it to EEPROM
*/
StartValues.initialDischargingMillivolt = aVoltageNoLoadMillivolt;
StartValues.initialDischargingMilliampere = aMilliampere;
StartValues.initialDischargingMilliohm = aMilliohm;
StartValues.compressionFlag = FLAG_NO_COMPRESSION;
StartValues.BatteryTypeIndex = sBatteryInfo.TypeIndex;
StartValues.DischargeMode = sDischargeMode;
StartValues.LoadResistorMilliohm = sCurrentLoadResistorAverage;
StartValues.CapacityMilliampereHour = 0; // Capacity is written at the end or computed while reading
eeprom_write_block(&StartValues, &EEPROMStartValues, sizeof(EEPROMStartValues));
/*
* Initially set up structure for delta storage
*/
ValuesForDeltaStorage.lastStoredVoltageNoLoadMillivolt = aVoltageNoLoadMillivolt;
ValuesForDeltaStorage.lastStoredMilliampere = aMilliampere;
ValuesForDeltaStorage.lastStoredMilliohm = aMilliohm;
ValuesForDeltaStorage.tempDeltaIsEmpty = true;
ValuesForDeltaStorage.compressionIsActive = false;
ValuesForDeltaStorage.DeltaArrayIndex = 0;
} else {
if (!ValuesForDeltaStorage.compressionIsActive) {
/*
* No compression -> use 8 bit deltas
*/
if ((unsigned int) ValuesForDeltaStorage.DeltaArrayIndex < MAX_NUMBER_OF_SAMPLES) {
if (!sOnlyPlotterOutput) {
Serial.print(F("Store 8 bit deltas:"));
}
/*
* Append value to delta values array
*/
int16_t tVoltageDelta = aVoltageNoLoadMillivolt - ValuesForDeltaStorage.lastStoredVoltageNoLoadMillivolt;
tVoltageDelta = storeDeltas(tVoltageDelta, &ValuesForDeltaStorage.tempVoltageDelta,
reinterpret_cast<uint8_t*>(&sMillivoltDeltaArrayEEPROM[ValuesForDeltaStorage.DeltaArrayIndex]));
ValuesForDeltaStorage.lastStoredVoltageNoLoadMillivolt += tVoltageDelta;
int16_t tMilliampereDelta = aMilliampere - ValuesForDeltaStorage.lastStoredMilliampere;
tMilliampereDelta = storeDeltas(tMilliampereDelta, &ValuesForDeltaStorage.tempMilliampereDelta,
reinterpret_cast<uint8_t*>(&sMilliampereDeltaArrayEEPROM[ValuesForDeltaStorage.DeltaArrayIndex]));
ValuesForDeltaStorage.lastStoredMilliampere += tMilliampereDelta;
int16_t tMilliohmDelta = aMilliohm - ValuesForDeltaStorage.lastStoredMilliohm;
tMilliohmDelta = storeDeltas(tMilliohmDelta, &ValuesForDeltaStorage.tempMilliohmDelta,
reinterpret_cast<uint8_t*>(&sMilliohmDeltaArrayEEPROM[ValuesForDeltaStorage.DeltaArrayIndex]));
ValuesForDeltaStorage.lastStoredMilliohm += tMilliohmDelta;
printlnIfNotPlotterOutput();
ValuesForDeltaStorage.DeltaArrayIndex++; // increase every sample
} else {
/*
* CONVERT
*/
if (!sOnlyPlotterOutput) {
Serial.println();
Serial.print(F("Convert "));
Serial.print(MAX_NUMBER_OF_SAMPLES);
Serial.println(F(" uncompressed stored values to compressed ones"));
}
// Start a new compressed storage
ValuesForDeltaStorage.DeltaArrayIndex = 0;
ValuesForDeltaStorage.compressionIsActive = true;
/*
* Read all data and process them. This recursively calls storeBatteryValuesToEEPROM(), but we end up below in code for compressed format
*/
sDoPrintCaption = false; // for recursive call to printValuesForPlotter() below
readAndProcessEEPROMData(true); // true -> convert data instead of printing them
sDoPrintCaption = true;
/*
* Clear rest of uncompressed EEPROM values
*/
for (unsigned int i = ValuesForDeltaStorage.DeltaArrayIndex; i < MAX_NUMBER_OF_SAMPLES; ++i) {
eeprom_update_byte(reinterpret_cast<uint8_t*>(&sMillivoltDeltaArrayEEPROM[i]), 0xFF);
eeprom_update_byte(reinterpret_cast<uint8_t*>(&sMilliampereDeltaArrayEEPROM[i]), 0xFF);
eeprom_update_byte(reinterpret_cast<uint8_t*>(&sMilliohmDeltaArrayEEPROM[i]), 0xFF);
}
/*
* Set compression flag
*/
StartValues.compressionFlag = FLAG_COMPRESSION;
eeprom_write_byte(&EEPROMStartValues.compressionFlag, FLAG_COMPRESSION); // store compression flag in EEPROM
if (!sOnlyPlotterOutput) {
Serial.println(F("Conversion done"));
Serial.println();
}
}
}
/*
* Check again, in order to store also the value which triggered the conversion
*/
if (ValuesForDeltaStorage.compressionIsActive) {
/*
* Store data in compressed format, 4 bit deltas
*/
if ((unsigned int) ValuesForDeltaStorage.DeltaArrayIndex < MAX_NUMBER_OF_SAMPLES) {
if (!sOnlyPlotterOutput) {
if (ValuesForDeltaStorage.tempDeltaIsEmpty) {
Serial.print(F("Store values to EEPROM compress buffer"));
} else {
Serial.print(F("Store values to EEPROM at index "));
Serial.print(ValuesForDeltaStorage.DeltaArrayIndex);
}
}
/*
* Append value to delta values array
*/
int16_t tVoltageDelta = aVoltageNoLoadMillivolt - ValuesForDeltaStorage.lastStoredVoltageNoLoadMillivolt;
tVoltageDelta = storeDeltas(tVoltageDelta, &ValuesForDeltaStorage.tempVoltageDelta,
reinterpret_cast<uint8_t*>(&sMillivoltDeltaArrayEEPROM[ValuesForDeltaStorage.DeltaArrayIndex]));
ValuesForDeltaStorage.lastStoredVoltageNoLoadMillivolt += tVoltageDelta;
int16_t tMilliampereDelta = aMilliampere - ValuesForDeltaStorage.lastStoredMilliampere;
tMilliampereDelta = storeDeltas(tMilliampereDelta, &ValuesForDeltaStorage.tempMilliampereDelta,
reinterpret_cast<uint8_t*>(&sMilliampereDeltaArrayEEPROM[ValuesForDeltaStorage.DeltaArrayIndex]));
ValuesForDeltaStorage.lastStoredMilliampere += tMilliampereDelta;
int16_t tMilliohmDelta = aMilliohm - ValuesForDeltaStorage.lastStoredMilliohm;
tMilliohmDelta = storeDeltas(tMilliohmDelta, &ValuesForDeltaStorage.tempMilliohmDelta,
reinterpret_cast<uint8_t*>(&sMilliohmDeltaArrayEEPROM[ValuesForDeltaStorage.DeltaArrayIndex]));
ValuesForDeltaStorage.lastStoredMilliohm += tMilliohmDelta;
printlnIfNotPlotterOutput();
if (ValuesForDeltaStorage.tempDeltaIsEmpty) {
ValuesForDeltaStorage.tempDeltaIsEmpty = false;
} else {
// start two new 4 bit compressed values
ValuesForDeltaStorage.DeltaArrayIndex++; // increase every second sample
ValuesForDeltaStorage.tempDeltaIsEmpty = true;
}
}
}
}
printValuesForPlotter(aVoltageNoLoadMillivolt, aMilliampere, aMilliohm, sDoPrintCaption);
Serial.println();
}
void storeCapacityAndDischargeModeToEEPROM() {
eeprom_write_word(&EEPROMStartValues.CapacityMilliampereHour, sBatteryInfo.CapacityMilliampereHour);
eeprom_write_byte(&EEPROMStartValues.DischargeMode, sDischargeMode);
if (!sOnlyPlotterOutput) {
// Print should be done after checkForDoublePress() in order to not disturb the double press detection
Serial.print(F("Discharge mode "));
Serial.print(getDischargeModeAsCharacter());
Serial.print(F(" and capacity "));
Serial.print(sBatteryInfo.CapacityMilliampereHour);
Serial.println(F(" mAh stored"));
}
#if defined(USE_LCD)
myLCD.setCursor(0, 0);
myLCD.print(F("Capacity stored "));
delay(LCD_MESSAGE_PERSIST_TIME_MILLIS);
#endif
}
/*
* The reproduced ESR is likely to be noisy if the relation between the load resistor and the ESR is big.
* E.g. for Li-ion we have an load resistor of 12.156 ohm and a voltage of 4.158 volt.
* We then get an ESR of 0.109 ohm for 339 mA and 0.073 ohm for 340 mA :-(.
*/
void printValuesForPlotter(uint16_t aVoltageToPrint, uint16_t aMilliampereToPrint, uint16_t aMilliohmToPrint,
bool aDoPrintCaption) {
#if defined(ARDUINO_2_0_PLOTTER_FORMAT)
Serial.print(F("Voltage:"));
Serial.print(aVoltageToPrint);
Serial.print(F(" Current:"));
Serial.print(aMilliampereToPrint);
Serial.print(F(" ESR:"));
Serial.print(aMilliohmToPrint);
if (aDoPrintSummary) {
// Print updated plotter caption
Serial.print(F(" Voltage="));
printMillisValueAsFloat(StartValues.initialDischargingMillivolt);
Serial.print(F("V->"));
printMillisValueAsFloat(aVoltageToPrint);
Serial.print(F("V__Current="));
Serial.print(StartValues.initialDischargingMilliampere);
Serial.print(F("mA->"));
Serial.print(aMilliampereToPrint);
Serial.print(F("mA__ESR="));
Serial.print(StartValues.initialDischargingMilliohm);
Serial.print(F("mohm->"));
Serial.print(aMilliohmToPrint);
Serial.print(F("mohm___LoadResistor="));
printMillisValueAsFloat(StartValues.LoadResistorMilliohm);
Serial.print(F("ohm__Capacity="));
Serial.print(StartValues.CapacityMilliampereHour);
Serial.print(F("mAh__Duration="));
// We have 2 4bit values per storage byte
uint16_t tDurationMinutes = (ValuesForDeltaStorage.DeltaArrayIndex)
* (2 * NUMBER_OF_SAMPLES_PER_STORAGE)/ SECONDS_IN_ONE_MINUTE;
Serial.print(tDurationMinutes / 60);
Serial.print(F("h_"));
Serial.print(tDurationMinutes % 60);
Serial.print(F("min:aVoltageToPrint"));
}
#else
if (aDoPrintCaption) {
// Print updated plotter caption
Serial.print(F("Voltage="));
printMillisValueAsFloat(StartValues.initialDischargingMillivolt);
Serial.print(F("V->"));
printMillisValueAsFloat(aVoltageToPrint);
Serial.print(F("V:"));
Serial.print(aVoltageToPrint);
Serial.print(F(" Current="));
// Serial.print(F("V Current="));
Serial.print(StartValues.initialDischargingMilliampere);
Serial.print(F("mA->"));
Serial.print(aMilliampereToPrint);
Serial.print(F("mA:"));
Serial.print(aMilliampereToPrint);
Serial.print(F(" ESR="));
// Serial.print(F("mA ESR="));
Serial.print(StartValues.initialDischargingMilliohm);
Serial.print(F("mohm->"));
Serial.print(aMilliohmToPrint);
Serial.print(F("mohm:"));
Serial.print(aMilliohmToPrint);
Serial.print(F(" LoadResistor="));
// Serial.print(F("mohm LoadResistor="));
printMillisValueAsFloat(StartValues.LoadResistorMilliohm);
Serial.print(F("ohm Capacity="));
if (sBatteryInfo.CapacityMilliampereHourStandardValueAtHighCutoff != 0
&& sBatteryInfo.CapacityMilliampereHourStandardValueAtHighCutoff != sBatteryInfo.CapacityMilliampereHour) {
Serial.print(sBatteryInfo.CapacityMilliampereHourStandardValueAtHighCutoff);
Serial.print('_');
}
Serial.print(sBatteryInfo.CapacityMilliampereHour);
Serial.print(F("mAh Duration"));
uint16_t tDurationMinutes;
if (StartValues.compressionFlag == FLAG_NO_COMPRESSION) {
// We have 1 8 bit delta value per storage byte
tDurationMinutes = (ValuesForDeltaStorage.DeltaArrayIndex) * (NUMBER_OF_SECONDS_PER_STORAGE / SECONDS_IN_ONE_MINUTE);
} else {
Serial.print(F("_compr"));
// We have 2 4 bit delta values per storage byte
tDurationMinutes = (ValuesForDeltaStorage.DeltaArrayIndex)
* (2 * NUMBER_OF_SECONDS_PER_STORAGE / SECONDS_IN_ONE_MINUTE);
}
Serial.print('=');
Serial.print(tDurationMinutes / 60);
Serial.print(F("h_"));
Serial.print(tDurationMinutes % 60);
Serial.print(F("min"));
} else {
Serial.print(aVoltageToPrint);
Serial.print(' ');
Serial.print(aMilliampereToPrint);
Serial.print(' ');
Serial.print(aMilliohmToPrint);
}
#endif
}
/*
* 6 is interpreted as 16 enabling values of 10 (16 -6) to 21 (16 +5) in 2 steps
* 7 is interpreted as 28 enabling values of 22 (28 -6) to 33 (28 +5) in 2 steps
* -7 is interpreted as -18 enabling values of -13 (-18 +5) to -24 (-18 -6) in 2 steps
* -8 is interpreted as -30 enabling values of -25 (-30 +5) to -36 (-30 -6) in 2 steps
*/
int8_t getDelta(uint8_t a4BitDelta) {
int8_t tDelta;
if (a4BitDelta == 15) {
tDelta = 28;
} else if (a4BitDelta == 14) {
tDelta = 16;
} else if (a4BitDelta == 1) {
tDelta = -18;
} else if (a4BitDelta == 0) {
tDelta = -30;
} else {
// Here values from 2 to 13 converted to -6 to 5
tDelta = a4BitDelta - 8;
}
return tDelta;
}
/*
* Copy EEPROM delta and start values to RAM
*/
void copyEEPROMDataToRam() {
eeprom_read_block(sVoltageDeltaArray, reinterpret_cast<uint8_t*>(&sMillivoltDeltaArrayEEPROM),
MAX_NUMBER_OF_SAMPLES);
eeprom_read_block(sMilliampereDeltaArray, reinterpret_cast<uint8_t*>(&sMilliampereDeltaArrayEEPROM),
MAX_NUMBER_OF_SAMPLES);
eeprom_read_block(sMilliohmDeltaArray, reinterpret_cast<uint8_t*>(&sMilliohmDeltaArrayEEPROM),
MAX_NUMBER_OF_SAMPLES);
/*
* Read start values data for later printing
*/
eeprom_read_block(&StartValues, &EEPROMStartValues, sizeof(EEPROMStartValues));
}
#define STANDARD_CAPACITY_UNITIALIZED 0
#define STANDARD_CAPACITY_ENABLED 1 // A Voltage >= NominalFullVoltageMillivolt was found
#define STANDARD_CAPACITY_STARTED 2 // Current voltage is below or equal NominalFullVoltageMillivolt and higher or equal SwitchOffVoltageMillivoltHigh
#define STANDARD_CAPACITY_COMPLETED 3 // Current voltage is below SwitchOffVoltageMillivoltHigh
/*
* Reads EEPROM delta values arrays
* - print data for plotter and compute ESR on the fly from voltage, current and load resistor
* - compute capacity from current (if defined SUPPORT_CAPACITY_RESTORE)
* - restore battery type and capacity accumulator as well as mAh
* - Capacity is stored in sBatteryInfo.CapacityMilliampereHour and sBatteryInfo.CapacityAccumulator
*/
void readAndProcessEEPROMData(bool aDoConvertInsteadOfPrint) {
/*
* First copy EEPROM delta and start values to RAM for later printing
*/
copyEEPROMDataToRam();
bool tIsCompressed = (StartValues.compressionFlag != FLAG_NO_COMPRESSION);
// search last non 0xFF (not cleared) value
int tLastNonZeroIndex;
for (tLastNonZeroIndex = (MAX_NUMBER_OF_SAMPLES - 1); tLastNonZeroIndex >= 0; --tLastNonZeroIndex) {
if (sVoltageDeltaArray[tLastNonZeroIndex] != 0xFF || sMilliampereDeltaArray[tLastNonZeroIndex] != 0xFF
|| sMilliohmDeltaArray[tLastNonZeroIndex] != 0xFF) {
break;
}
}
tLastNonZeroIndex++; // Convert from 0 to MAX_NUMBER_OF_SAMPLES-1 to ValuesForDeltaStorage.DeltaArrayIndex to 0 to MAX_NUMBER_OF_SAMPLES
sDischargeMode = StartValues.DischargeMode;
if (StartValues.BatteryTypeIndex <= TYPE_INDEX_MAX) {
sBatteryInfo.TypeIndex = StartValues.BatteryTypeIndex;
} else {
sBatteryInfo.TypeIndex = TYPE_INDEX_DEFAULT; // value for uninitialized EEPROM
}
uint16_t tVoltage = StartValues.initialDischargingMillivolt;
uint8_t tCapacityMilliampereHourStandardValueState = STANDARD_CAPACITY_UNITIALIZED;
// Check if start voltage > voltage for standard capacity computation
if (tVoltage >= BatteryTypeInfoArray[sBatteryInfo.TypeIndex].NominalFullVoltageMillivolt) {
tCapacityMilliampereHourStandardValueState = STANDARD_CAPACITY_ENABLED;
}
uint16_t tMilliampere = StartValues.initialDischargingMilliampere;
uint16_t tMilliohm = StartValues.initialDischargingMilliohm;
sBatteryInfo.Milliohm = tMilliohm; // displayed in summary print
sBatteryInfo.Milliampere = tMilliampere; // To avoid overflow detection for Milliohm print
sBatteryInfo.CapacityMilliampereHour = StartValues.CapacityMilliampereHour; // required for printing and append to EEPROM functionality
sBatteryInfo.CapacityMilliampereHourStandardValueAtHighCutoff = 0;
// Required for conversion
ValuesForDeltaStorage.lastStoredVoltageNoLoadMillivolt = tVoltage;
ValuesForDeltaStorage.lastStoredMilliampere = tMilliampere;
ValuesForDeltaStorage.lastStoredMilliohm = tMilliohm;
if (!sOnlyPlotterOutput) {
Serial.println();
// We have always the first one as uncompressed value
if (tIsCompressed) {
Serial.print(tLastNonZeroIndex * 2);
Serial.print(' ');
} else {
Serial.print(tLastNonZeroIndex + 1);
Serial.print(F(" un"));
}
Serial.print(F("compressed EEPROM values found for type="));
Serial.print(BatteryTypeInfoArray[sBatteryInfo.TypeIndex].TypeName);
Serial.print(F(" and discharge mode="));
Serial.println(getDischargeModeAsCharacter());
}
uint32_t tCapacityAccumulator = tMilliampere;
uint32_t tCapacityAccumulatorStartForStandardValue = 0;
if (!aDoConvertInsteadOfPrint) {
/*
* Print the initial value and no caption to plotter
*/
printValuesForPlotter(tVoltage, tMilliampere, tMilliohm, false);
Serial.println();
// Required for appending to compressed values
ValuesForDeltaStorage.DeltaArrayIndex = tLastNonZeroIndex;
ValuesForDeltaStorage.compressionIsActive = tIsCompressed;
}
// DeltaArrayIndex can be from 0 to MAX_NUMBER_OF_SAMPLES
for (int i = 0; i < tLastNonZeroIndex; ++i) {
if (!tIsCompressed) {
/*
* Uncompressed
*/
#if defined(DEBUG)
if (!sOnlyPlotterOutput) {
Serial.print(F("EEPROM values="));
Serial.print((int8_t) sVoltageDeltaArray[i]);
Serial.print(' ');
Serial.print((int8_t) sMilliampereDeltaArray[i]);
Serial.print(' ');
Serial.println((int8_t) sMilliohmDeltaArray[i]);
}
#endif
tVoltage += (int8_t) sVoltageDeltaArray[i];
tMilliampere += (int8_t) sMilliampereDeltaArray[i];
tMilliohm += (int8_t) sMilliohmDeltaArray[i];
if (aDoConvertInsteadOfPrint) {
/*
* Convert uncompressed values here
*/
storeBatteryValuesToEEPROM(tVoltage, tMilliampere, tMilliohm);
// Serial.print(F("Stored result "));
// printValuesForPlotter(ValuesForDeltaStorage.lastStoredVoltageNoLoadMillivolt, ValuesForDeltaStorage.lastStoredMilliampere, ValuesForDeltaStorage.lastStoredMilliohm, false);
}
tCapacityAccumulator += tMilliampere; // putting this into printValuesForPlotter() increases program size
} else {
/*
* Compressed
*/
#if defined(DEBUG)
if (!sOnlyPlotterOutput) {
Serial.print(F("EEPROM values=0x"));
Serial.print(sVoltageDeltaArray[i], HEX);
Serial.print(F(" 0x"));
Serial.print(sMilliampereDeltaArray[i], HEX);
Serial.print(F(" 0x"));
Serial.println(sMilliohmDeltaArray[i], HEX);
}
#endif
/*
* Process first part of compressed data
*/
uint8_t t4BitVoltageDelta = sVoltageDeltaArray[i];
tVoltage += getDelta(t4BitVoltageDelta >> 4);
uint8_t t4BitMilliampereDelta = sMilliampereDeltaArray[i];
tMilliampere += getDelta(t4BitMilliampereDelta >> 4);
uint8_t t4BitMilliohmDelta = sMilliohmDeltaArray[i];
tMilliohm += getDelta(t4BitMilliohmDelta >> 4);
tCapacityAccumulator += tMilliampere; // putting this into printValuesForPlotter() increases program size
/*
* Print first part of expanded values
*/
if (!sOnlyPlotterOutput || tLastNonZeroIndex < (MAX_VALUES_DISPLAYED_IN_PLOTTER / 2)) {
/*
* Skip every second value, if we have more than 500 uncompressed (250 compressed) values,
* to fit the graph into the plotter display.
* Skip first part of compressed data, since we printed initial value before loop
*/
printValuesForPlotter(tVoltage, tMilliampere, tMilliohm, false);
Serial.println();
}
/*
* Process second part of compressed data
*/
tVoltage += getDelta(t4BitVoltageDelta & 0x0F);
tMilliampere += getDelta(t4BitMilliampereDelta & 0x0F);
tMilliohm += getDelta(t4BitMilliohmDelta & 0x0F);
/*
* Restoring capacity value from stored data, which have a bigger sample interval than measured data.
* The observed delta was around 1%, so we can use this as a fallback, if no capacity data was stored e.g. in case of a sudden power down.
* Increases program size by 184 bytes.
*/
tCapacityAccumulator += tMilliampere;
}
if (!aDoConvertInsteadOfPrint) {
uint16_t tVoltageForPrint = tVoltage; // To print markers for start and end of standard capacity
uint8_t tPrintDelayed = 0;
/*
* Get "standard" capacity from NominalFullVoltageMillivolt to SwitchOffVoltageMillivoltHigh
*/
if (tCapacityMilliampereHourStandardValueState == STANDARD_CAPACITY_UNITIALIZED
&& tVoltage >= BatteryTypeInfoArray[sBatteryInfo.TypeIndex].NominalFullVoltageMillivolt) {
tCapacityMilliampereHourStandardValueState = STANDARD_CAPACITY_ENABLED;
}
if (tCapacityMilliampereHourStandardValueState == STANDARD_CAPACITY_ENABLED
&& tVoltage <= BatteryTypeInfoArray[sBatteryInfo.TypeIndex].NominalFullVoltageMillivolt) {
// Store initial capacity at starting point
tCapacityMilliampereHourStandardValueState = STANDARD_CAPACITY_STARTED;
tCapacityAccumulatorStartForStandardValue = tCapacityAccumulator;
tPrintDelayed = 1; // print text after print of values
if (sOnlyPlotterOutput) {
tVoltageForPrint += 50; // modify voltage before print of values
}
} else if (tCapacityMilliampereHourStandardValueState == STANDARD_CAPACITY_STARTED
&& tVoltage < BatteryTypeInfoArray[sBatteryInfo.TypeIndex].SwitchOffVoltageMillivoltHigh) {
tCapacityMilliampereHourStandardValueState = STANDARD_CAPACITY_COMPLETED;
sBatteryInfo.CapacityMilliampereHourStandardValueAtHighCutoff = (tCapacityAccumulator
- tCapacityAccumulatorStartForStandardValue) / NUMBER_OF_STORAGES_PER_HOUR; // -> tCapacityAccumulator / 60
if (i != tLastNonZeroIndex - 1) { // do not modify last value line containing caption
tPrintDelayed = 2; // print text after print of values
if (sOnlyPlotterOutput) {
tVoltageForPrint += 50; // modify voltage before print of values
}
}
}
/*
* Print (the second uncompressed) values
* At last, print the caption with values from the end of the measurement cycle to plotter
*/
printValuesForPlotter(tVoltageForPrint, tMilliampere, tMilliohm, (i == tLastNonZeroIndex - 1));
/*
* append
*/
if (tPrintDelayed == 1) {
if (!sOnlyPlotterOutput) {
Serial.print(F(" - Capacity on top of standard value="));
Serial.print(tCapacityAccumulator / NUMBER_OF_STORAGES_PER_HOUR);
Serial.print(F(" mAh"));
}
} else if (tPrintDelayed == 2) {
if (!sOnlyPlotterOutput) {
Serial.print(F(" - Standard capacity="));
Serial.print(sBatteryInfo.CapacityMilliampereHourStandardValueAtHighCutoff);
Serial.print(F(" mAh"));
}
}
Serial.println();
}
}
/*
* Loop was processed, handle capacity and LCD display now
*/
if (!aDoConvertInsteadOfPrint) {
uint16_t tCurrentCapacityMilliampereHourComputed = tCapacityAccumulator / NUMBER_OF_STORAGES_PER_HOUR;
if (!sOnlyPlotterOutput) {
if (sBatteryInfo.CapacityMilliampereHour == 0) {
Serial.print(F("No capacity was stored, so use computed capacity of "));
Serial.print(tCurrentCapacityMilliampereHourComputed);
} else {
/*
* The observed delta was around 1% :-)
*/
int16_t tCurrentCapacityMilliampereHourDelta = sBatteryInfo.CapacityMilliampereHour
- tCurrentCapacityMilliampereHourComputed;
Serial.print(F("Stored minus computed capacity="));
Serial.print(tCurrentCapacityMilliampereHourDelta);
}
Serial.println(F(" mAh"));
/*
* Print Standard capacity a between NominalFullVoltageMillivolt and SwitchOffVoltageMillivoltHigh,
* if we have both values.
*/
if (sBatteryInfo.CapacityMilliampereHourStandardValueAtHighCutoff != 0
&& sBatteryInfo.CapacityMilliampereHourStandardValueAtHighCutoff != tCurrentCapacityMilliampereHourComputed) {
Serial.print(F("Standard computed capacity between "));
Serial.print(BatteryTypeInfoArray[sBatteryInfo.TypeIndex].NominalFullVoltageMillivolt);
Serial.print(F(" V and "));
Serial.print(BatteryTypeInfoArray[sBatteryInfo.TypeIndex].SwitchOffVoltageMillivoltHigh);
Serial.print(F(" V="));
Serial.print(sBatteryInfo.CapacityMilliampereHourStandardValueAtHighCutoff);
Serial.println(F(" mAh"));
}
} // if (!sOnlyPlotterOutput)
if (sBatteryInfo.CapacityMilliampereHour == 0) {
sBatteryInfo.CapacityMilliampereHour = tCurrentCapacityMilliampereHourComputed;
}
// restore capacity accumulator
sBatteryInfo.CapacityAccumulator = sBatteryInfo.CapacityMilliampereHour
* ((3600L * MILLIS_IN_ONE_SECOND) / SAMPLE_PERIOD_OF_LOAD_ACIVATED_MILLIS);
// if (!sOnlyPlotterOutput) {
// Serial.print(F("EEPROM values: "));
// }
/*
* Store current values in structure for delta storage for append functionality
*/
ValuesForDeltaStorage.lastStoredVoltageNoLoadMillivolt = tVoltage;
ValuesForDeltaStorage.lastStoredMilliampere = tMilliampere;
ValuesForDeltaStorage.lastStoredMilliohm = tMilliohm;
ValuesForDeltaStorage.tempDeltaIsEmpty = true;
}
}
void TogglePin(uint8_t aPinNr) {
if (digitalRead(aPinNr) == HIGH) {
digitalWrite(aPinNr, LOW);
} else {
digitalWrite(aPinNr, HIGH);
}
}
void LCDClearLine(uint8_t aLineNumber) {
#if defined(USE_LCD)
myLCD.setCursor(0, aLineNumber);
myLCD.print(" ");
myLCD.setCursor(0, aLineNumber);
#else
(void) aLineNumber;
#endif
}
Voltage.
Current.
Cutoff low
Stop / Start
Plotter output
To simulate a battery, select the right voltage at one simulation and then start a new simulation