/*
Trolling motor battery sensor
Arduino nano with ACS712 (30A) Hall_sensor, LCD_displayI2C_20x4
and SD card module.
Keeps track of amps, volts and watts used.
Every second stores values on the SD card.
Can store in the EEPROM the watts used
and use them for the next run of the motor.
=== More details in the Wattmeter for trolling motor.pdf file ===
INSTRUCTIONS:
1. Connect arduino and motor to the battery;
2. Wait for initial setup with the motor OFF;
3. Follow instructions on the lcd and press button
if you have stored values from previous motor runs (8 secs time window);
4. Turn ON the motor and go;
5. Before ending the run and if you want a second run later,
press button to save used watts and turn off arduino.
LED status:
-------BLUE: OFF = All is well, ON = Danger
-------BLUE (on remote): dim light = amps < 9A, bright light = amps > 11.5A, off = above 9A and below 11.5A
S. Sotiriadis Oct 2023
USE AT YOU OWN RISK, ETC,ETC. BATTERIES ARE VERY WEIRD CREATURES.
Endnote:
I've written this sketch for my personal use, the long explanations in the code
are for the future me, much older and probably even more mentally challenged.
*/
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include <SPI.h>
#include <SD.h>
/*
SD card module
SD card attached to SPI bus as follows:
** MOSI - pin 11
** MISO - pin 12
** CLK - pin 13
** CS - pin 10 <===== Change according to free pins
*/
LiquidCrystal_I2C lcd(0x27, 20, 4); // Arduino pins: SDA -> A4, SCL-> A5
// -------------PINS
const byte cardCS = 10; // Arduino pin connected to (CS)of Sd module
const byte blueLed = 8;
const byte blueLedRemote = 6; // This led goes with the external PWM knob on the wired remote control case
const byte button = 2; // Button pin
const byte ampSensor = A0; // Data pin of ACS712
const byte voltSensor = A3; // Battery[+] --> R1(220K) --> A3(Vout) --> R2(100K) --> common ground[-]
// SDA Lcd - A4,
// SCL Lcd - A5,
// MOSI Sd module - 11,
// MISO Sd module - 12,
// CLK Sd module - 13.
/*
A not so scientific multiple linear regression regarding discharge amps - volts and SoC:
State of Charge % = varAmps * Amps + varVolts * Voltage + intercept
I know it shouldn't be linear. However it's a good fit for 100 to 70 SoC% from 6 up to 12-15 Amps
for my 60Ah battery. Use the Excel file to calculate your own values.
Voltage under C/5 draw
10-12 A for 60Ah 12V battery:
>= 12.10V - 100%,
12.00V - 90%,
11.90V - 80%,
11.80V - 70%, <== Limit I suggest for V under load
11.68V - 60%,
11.50V - 50%.
Starting Voltage, no draw:
>= 12.7 - 100%,
12.6 - 90%,
12.5 - 80%,
12.4 - 70%,
12.3 - 60%,
12.2 - 50%.
*/
// ------------USER DEFINED CONSTANTS
// Use the Excel file to calculate your own values (the ones bellow are for a 60Ah battery)
volatile int wattLimit = 240; // My suggested watts to be drawn = 1/3 of max Watts (60Ah 12V battery in theory = 720W)
const byte suggestedAmps = 12; //My suggested max amps to use (C/5 for above mentioned battery)
float varAmps = 7.531; // See equation above
float varVolts = 99.724; // See equation above
float intercept = -1194.28; // See equation above
// ------------VARIABLES
int SoC = 0; // State of Charge guess
volatile bool buttonPress; // Flag the button press
const byte mvAmp = 66; // 30A version of ACS712 1A every 66mV rise
float hallVolt = 0.0; // Voltage of hall sensor data pin
unsigned long ampTot = 0; // Cumulative value of hall sensor readings
float Amps = 0.0; // Amps drawn
float voltReference = 0.0; // Voltage of hall sensor data pin when no current (I consider the arduino consumption of 25-30mA negligible)
float Watt = 0; // Watts hour
float wattTotal = 0.0; // Watts used
float wattTotalOld = 0.0; // Total watts used before the previous minute
float wattTotxmin = 0.0; // Total watts used in the last minute
int min15Amps = 0; // Suggested amps for 15 minutes autonomy
float wattsRemaining = 0.0; // Watts remaining until reaching battery limit
int minutesRemaining = 0; // Minutes until reaching battery limit
float vout; // Voltage output from voltage divider
float vBat = 0.0; // Real time battery voltage (last second)
float vstart = 0.0; // Initial battery voltage (no load)
float R1 = 220000.0; // Resistance R1 (220K)
float R2 = 100700.0; // Resistance R2 (100K)
bool danger = false; // If true modify line 4 of LCD
File myFile; // File for Sd card
String message; // LCD line 4 rolling message
bool halfWay = false; // When used half battery juice, point of no return
bool cumulative = false; // Flag the return leg (if any)
bool currentDraw = false; // Flag the excessive current use
bool batteryGone = false; // Flag battery problems
int newDrop; // Battery voltage drop/minute in mV
int oldvBat; // Previous minute battery voltage in mV
int oldDrop; // Previous battery voltage drop/minute in mV
int dropDiff; // Voltage drop difference in mV
float secondAmps; // Sum of Amps every second
int minuteAmps; // Average of Amps used in the last minute
int oldmAmps; // Previous period average Amps
//------------------- Package for eeprom
struct package {
byte checkValue; // predifined value (I used 106)
int Remaining; // Used watts
int MinutesRunning; // Minutes using the motor
};
typedef struct package Package1;
Package1 eeprom_data;
//----------------- Lcd custom characters
byte charBattery[] = { B01010,
B11111,
B10001,
B10001,
B10001,
B11111,
B11111,
B11111 };
byte charAmps[] = { B10001,
B01010,
B00100,
B00000,
B01110,
B01010,
B01110,
B01010 };
byte charOK[] = { B00000,
B11011,
B11011,
B00000,
B00100,
B10001,
B01010,
B00100 };
byte charDanger[] = { B01110,
B11111,
B10101,
B11111,
B11011,
B01110,
B01110,
B00000 };
byte charSave[] = { B01110,
B10101,
B10101,
B11111,
B10101,
B10101,
B01110,
B00000 };
byte charNext[] = { B11111,
B10001,
B10101,
B10001,
B10011,
B10101,
B10101,
B11111 };
byte firstRun[] = { B11111,
B10001,
B10111,
B10111,
B10011,
B10111,
B10111,
B11111 };
// ------------- VARIOUS COUNTERS & timers
int count = 0;
byte secondCount = 0; // Seconds count
int minuteCount = 0;
byte screenCount = 0; // Refresh lcd with new messages
byte messageCount = 0; // Alternate messages on lcd
unsigned long start_time = millis();
unsigned long start_arduino = 0; // Start timer after the 1 sec iteration
unsigned long end_arduino = 0; // Stop timer at the end of the loop
void setup() {
//--------------- Define pinMode
pinMode(ampSensor, INPUT);
pinMode(voltSensor, INPUT);
pinMode(blueLed, OUTPUT);
pinMode(blueLedRemote, OUTPUT);
pinMode(button, INPUT_PULLUP);
//----------------- Set initial state of pins & button flag
digitalWrite(blueLed, LOW);
digitalWrite(blueLedRemote, LOW);
buttonPress = false;
//Start Interrupt sensing
attachInterrupt(digitalPinToInterrupt(button), bPress, FALLING);
//Start LCD
lcd.init();
lcd.backlight();
//Load custom characters to lcd
lcd.createChar(0, charOK);
lcd.createChar(1, charAmps);
lcd.createChar(2, charBattery);
lcd.createChar(3, charDanger);
lcd.createChar(4, charSave);
lcd.createChar(5, charNext);
lcd.createChar(6, firstRun);
// Start Sd module and check it
if (!SD.begin(cardCS)) {
lcd.setCursor(0, 0);
lcd.print(F("SD Module Failed!"));
lcd.setCursor(0, 2);
lcd.print(F("Press B to continue"));
while (!buttonPress) {
// press button to continue without card module
}
lcd.clear();
buttonPress = false;
}
// Check for card presence
myFile = SD.open("BATT_RUN.TXT", FILE_WRITE);
if (!myFile) {
lcd.clear();
lcd.setCursor(4, 0);
lcd.print(F("NO SD CARD"));
lcd.setCursor(0, 2);
lcd.print(F("Press B to continue"));
while (!buttonPress) {
// press button to continue without microSD card
}
lcd.clear();
buttonPress = false;
}
myFile.close();
lcd.setCursor(6, 0);
lcd.print(F(" NO LOAD "));
lcd.setCursor(2, 1);
lcd.print(F("CALIBRATION"));
delay(3000); // Give some more time to hall sensor
lcd.clear();
/*
=============================================================
3 second calibration of Hall sensor without load and battery
voltage under minimum load (just the draw of arduino)
=============================================================
*/
lcd.setCursor(0, 0);
lcd.println(F("****MOTOR OFF*****"));
lcd.setCursor(0, 1);
lcd.print(F("after this sreen"));
lcd.setCursor(0, 2);
lcd.print(F("PRESS button to USE"));
lcd.setCursor(0, 3);
lcd.print(F("previous W values"));
// Starting timer
start_time = millis();
// Starting sampling ports A1 and A3
while ((millis() - start_time) < 3000) {
ampTot += analogRead(ampSensor); // using ampTot to store cumulative hall voltage readings
vout += analogRead(voltSensor); // cumulative battery voltage readings
count = count + 1;
delay(0.2);
}
// Averaging
voltReference = ((ampTot / count) * 5.0) / 1024.0;
vout = ((vout / count) * 5.0) / 1024.0;
vstart = vout / (R2 / (R1 + R2));
// Computing initial state of charge (a guess)
lowCurrent(vstart);
//LINE 1 - Part of the Line doesn't change during the sketch run (0 to 10 chars)
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(F("Vo "));
lcd.print(vstart, 1);
lcd.setCursor(8, 0);
lcd.print(F("SoC"));
lcd.print(SoC);
lcd.print(F("%"));
//LINE 2
lcd.setCursor(0, 1);
lcd.print(F("*** 8 sec DELAY ***"));
//LINE 3
lcd.setCursor(0, 2);
lcd.print(F("TURN ON MOTOR, PRESS"));
//LINE 4
lcd.setCursor(0, 3);
lcd.print(F("button TO ADD prev W"));
delay(8000); // The interrupt routine still works during the delay
if (buttonPress) {
EEPROM.get(0, eeprom_data); // Read eeprom data
buttonPress = false; // Reset button flag
if (eeprom_data.checkValue == 106) { // Check the control number
wattLimit = eeprom_data.Remaining; // New value of limit watts after the first leg
minuteCount = eeprom_data.MinutesRunning; // Add minutes of previous run
eeprom_data.checkValue = 0; // Reset
eeprom_data.Remaining = 0; // Reset
eeprom_data.MinutesRunning = 0; // Reset
EEPROM.put(0, eeprom_data); // Save to eeprom empty struct
lcd.setCursor(19, 2);
lcd.write(5); // Line 3, last position, print consecutive run symbol
cumulative = true;
}
} else {
lcd.setCursor(19, 2);
lcd.write(6); // Line 3, last position, print first run symbol
}
// Write initial data on fileName
myFile = SD.open("BATT_RUN.TXT", FILE_WRITE);
if (myFile) {
myFile.print(F("New Run ====== Initial voltage = "));
myFile.print(vstart, 2);
myFile.print(F(" , SoC = "));
myFile.print(SoC);
myFile.print(F(" , Hall sensor voltage = "));
myFile.print(voltReference, 2);
myFile.print(F(" , remaining watts = "));
myFile.println(wattLimit);
myFile.println(F("Volts, Amps, SoC %, Used W, Minutes"));
}
myFile.close();
// All said and done you have to wait about 14 secs for this part
// when microSD module and card are OK
}
void loop() {
// =========Resetting cumulative values==============
ampTot = 0;
vout = 0.0;
count = 0;
// ===============Starting timer=====================
start_time = millis();
// =========Starting sampling (for 1 sec)============
while ((millis() - start_time) < 1000 - (end_arduino - start_arduino)) //1 sec sampling minus time used for code running
{
ampTot += analogRead(ampSensor);
vout += analogRead(voltSensor);
count = count + 1;
delay(0.2);
}
/*
From here the loop runs once every second
*/
start_arduino = millis(); // Start timer for code running
screenCount += 1; //Count seconds for screen messages
secondCount += 1; // Count seconds
// ============= Calculations per second =====================
vout = ((vout / count) * 5.0) / 1024.0;
vBat = vout / (R2 / (R1 + R2));
hallVolt = ((ampTot / count) * 5.0) / 1024.0;
int absolute = round(hallVolt * 1000) - round(voltReference * 1000);
hallVolt = abs(absolute); // ATTENTION: used the same variable name to reduce memory footprint
Amps = hallVolt / mvAmp;
secondAmps += Amps;
Watt = vBat * Amps;
wattTotal += Watt / 3600.0;
// ===== Do the maths and more (or less)=============
// Works pretty good above 5-6 amps
if (Amps >= 5) {
SoC = round(varAmps * Amps + varVolts * vBat + intercept);
if (SoC > 100 || SoC < 0) {
SoC = 100;
}
} else {
lowCurrent(vBat - (0.01 * round(Amps))); // For low amps calculate SoC almost as in no load conditions
// The problem is that battery does not recover instantly
// its voltage when lowering the load. Give it some time.
}
wattsRemaining = wattLimit - wattTotal;
// This one depends on a variable (the wattTotxmin) that changes every minute only,
// so between minutes it has the previous minute value.
if (wattTotxmin > 0) {
minutesRemaining = round(wattsRemaining / wattTotxmin);
min15Amps = wattsRemaining / (vBat * 4);
} else {
minutesRemaining = 999; // when there is no draw in the last minute use 999
min15Amps = wattsRemaining / (vBat * 4);
}
myFile = SD.open("BATT_RUN.TXT", FILE_WRITE);
if (myFile) {
myFile.print(vBat); // Last second voltage
myFile.print(F(","));
myFile.print(Amps); // Last second average amperage used
myFile.print(F(","));
myFile.print(SoC); // Last second SoC%
myFile.print(F(","));
myFile.print(wattTotal); // Total watts used up to the last second
myFile.print(F(","));
myFile.println(minuteCount); // Minutes of use
}
myFile.close();
// vvvvvvvvvvvvvvvv START minute calculations vvvvvvvvvvvvvvv
// This part only works when there is a constant high amp draw
// Not very happy with it. Maybe it works with old batteries
if (secondCount == 60) {
secondCount = 0; // Reset seconds
minuteCount += 1; // Increment minute count
wattTotxmin = wattTotal - wattTotalOld;
wattTotalOld = wattTotal;
minuteAmps = round(secondAmps / 60);
// Be careful here: when reducing the amps or stopping the motor
// battery voltage will get higher, so voltage drop difference will
// be negative (both newDrop and dropDiff)
newDrop = oldvBat - round(vBat * 1000); // Voltage drop per minute (mV/min) now
if (newDrop > 11000 || newDrop < 0) { // Remove calcs when oldvBat or vBat is very low or zero
newDrop = 0;
}
oldvBat = round(vBat * 1000); // Set Voltage base for next minute calcs (mV/min)
dropDiff = newDrop - oldDrop; // Difference of voltage drop (mV/min)
if (dropDiff < 0) { // Remove errors as explained above
dropDiff = 0;
}
oldDrop = newDrop; // Set voltage drop for next minute calcs (mV/min)
/* I consider the next part to be the most important for checking the battery's health.
Assuming a continuous draw of Amps during your trips you can study the data on the microSD
when running with a new, or almost new battery and determine the normal voltage drop/min
Here I assume that my battery's Vdrop is normally 5-5.5 mV/min when I draw around 10 Amps.
So any abnormal drop (absolute or relative to the previous minute) raises a flag.
Modify to match the Vdrop/min of your normally drawn Amps.
I repeat that it only works when there is a constant amps draw.
*/
if (minuteAmps > 9 && oldmAmps > 9 && abs(minuteAmps - oldmAmps) < 2) { // Only when there is less than 2 Amps difference from previous minute
// and both old and new values are above 9 amps.
if (newDrop > 6 && dropDiff > 3) { // Here you can change both values (6 & 3) after experimentation
batteryGone = true;
} else {
batteryGone = false;
}
} else {
batteryGone = false;
}
secondAmps = 0; // reset sum of Amps per second
oldmAmps = minuteAmps; // Set value for next minute calculations
}
// ^^^^^^^^^^^^^^^^ END minute calculations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// =========== The various conditionals (again every second) ============
// If button is pressed arduino stores the watts used to eeprom.
// Mind that although the button sensing is instantaneous (interrupt routine)
// the code below will run AFTER the 1 sec iteration above. So, just wait
// for the symbol to appear on the lcd.
if (buttonPress) {
eeprom_data.checkValue = 106;
eeprom_data.Remaining = round(wattsRemaining);
eeprom_data.MinutesRunning = minuteCount;
buttonPress = false;
EEPROM.put(0, eeprom_data);
lcd.setCursor(19, 2); // Line 3, last position, print saved to eeprom symbol
lcd.write(4);
}
// When half or more battery juice is used
if (wattsRemaining <= (wattLimit / 2)) {
halfWay = true;
}
// Turn on/off the amps char on lcd on Amps usage
if (round(Amps) >= suggestedAmps) {
currentDraw = true;
} else {
currentDraw = false;
}
// Bright green led if amps between 9A and 11.5A
// Dim green led if amps below 9A
// No light if above 11.5A
if (Amps >= 9.0 && Amps <= 11.5) {
analogWrite(blueLedRemote, 0);
} else if (Amps < 9.0) {
analogWrite(blueLedRemote, 10);
} else {
analogWrite(blueLedRemote, 200);
}
// Turn on/off the panic mode
if (wattsRemaining < 70.0 || SoC < 80) { // if you are intrepid change OR to AND
danger = true;
} else {
danger = false;
}
// Turn the blue led on and off
if (batteryGone || danger || currentDraw) {
digitalWrite(blueLed, HIGH);
} else {
digitalWrite(blueLed, LOW);
}
// Every 3 secs change the message of LCD's LINE 4
if (screenCount == 3) {
messageChange();
screenCount = 0;
}
// ----- LCD VISUALIZATION --------
//LINE 1 (last 9 chars)
lcd.setCursor(11, 0);
lcd.print(F(" "));
lcd.setCursor(11, 0);
lcd.print(SoC);
lcd.print(F("% T:"));
if (SoC < 100) {
if (minuteCount < 10) {
lcd.print(F("00"));
lcd.print(minuteCount);
} else if (minuteCount < 100) {
lcd.print(F("0"));
lcd.print(minuteCount);
} else if (minuteCount >= 100) {
lcd.print(minuteCount);
}
} else if (SoC >= 100) {
if (minuteCount < 10) {
lcd.print(F("0"));
lcd.print(minuteCount);
} else {
lcd.print(minuteCount);
}
}
//LINE 2
lcd.setCursor(0, 1);
lcd.print(F(" "));
lcd.setCursor(0, 1);
lcd.print(F("V="));
lcd.print(vBat, 1);
lcd.print(F(" Vd="));
lcd.print(vstart - vBat, 2); //
if (halfWay) {
lcd.print(F(" HALF")); // Reached the point of no return
}
//LINE 3
lcd.setCursor(0, 2);
lcd.print(F(" ")); // Leave last 4 spaces for special characters
lcd.setCursor(0, 2);
lcd.print(F("A="));
lcd.print(Amps, 1);
lcd.print(F(" W/h="));
lcd.print(Watt, 1);
lcd.setCursor(16, 2);
lcd.print(" "); // Clear symbols
lcd.setCursor(16, 2);
if (currentDraw) {
lcd.write(1); // line 3, position 17
} else {
lcd.write(byte(0));
}
if (batteryGone) {
lcd.write(2); // line 3, position 18
} else {
lcd.write(byte(0));
}
if (danger) {
lcd.write(3); // line 3, position 19
} else {
lcd.write(byte(0));
}
//LINE 4
lcd.setCursor(0, 3);
lcd.print(F(" "));
lcd.setCursor(0, 3);
lcd.print(message);
end_arduino = millis(); // stop timer
}
//Function for ISR
void bPress() {
buttonPress = true;
}
// Create string for the alternating message displayed in LINE 4
void messageChange() {
if (!danger) { //Messages when all OK
if (messageCount == 0) {
message = "mVdrop/m " + String(newDrop);
} else if (messageCount == 1) {
if (minuteCount < 10) {
message = "Time running 00" + String(minuteCount);
} else if (minuteCount < 100) {
message = "Time running 0" + String(minuteCount);
} else if (minuteCount >= 100) {
"Time running " + String(minuteCount);
}
} else if (messageCount == 2) {
message = "Used Watts " + String(round(wattTotal));
} else if (messageCount == 3) {
message = "Minutes to go " + String(minutesRemaining);
} else if (messageCount == 4) {
message = "Watts to use " + String(round(wattsRemaining));
} else if (messageCount == 5) {
message = String((wattTotxmin * 1000)) + "mW/m " + String(Amps) + "A";
}
} else { // Messages when problems
if (messageCount == 0) {
message = "Panic not, WARNING";
} else if (messageCount == 1) {
message = "Amps for 15m ride " + String(min15Amps);
} else if (messageCount == 2) {
message = "Watts to use " + String(round(wattsRemaining));
} else if (messageCount == 3) {
message = "WARNING: " + String(minutesRemaining) + "m left";
} else if (messageCount == 4) {
message = "Amps for 15m ride " + String(min15Amps); // Repeat
} else if (messageCount == 5) {
message = String((wattTotxmin * 1000)) + "mW/m " + String(Amps) + "A";
}
}
messageCount += 1;
if (messageCount == 6) { messageCount = 0; }
}
// Computing no load (or almost) state of charge
void lowCurrent(float volts) {
if (volts >= 12.65) {
SoC = 100;
} else if (volts < 12.65 && volts >= 12.55) {
SoC = 90;
} else if (volts < 12.55 && volts >= 12.45) {
SoC = 80;
} else if (volts < 12.45 && volts >= 12.35) {
SoC = 70;
} else if (volts < 12.35 && volts >= 12.25) {
SoC = 60;
} else if (volts < 12.25 && volts >= 12.15) {
SoC = 50;
} else {
SoC = 40;
}
}
Comments
Please log in or sign up to comment.