Voltage under load is incorrect

I've built an RC battery discharge for 1s and 2s Lipo batteries. It has been an extremely fun project, but there have been slight issues along the way.

The current issue I am working on is the voltage under load. Reading the battery voltage with no load returns decently accurate results, but once I put a load on the battery, which is the idea, the reading is incorrect. How incorrect the reading is, depends on how big the load it. At 10A, the reading is off by about 0.04V. At 30A it is off by about 0.10V. Most of this seems to be in cell 2.

The voltage readings are done through a voltage divider of 2 5k1 resistors. I also added a filter cap of 1uF. The readings are averaged a lot, lol. But I have tried no average and high averages. I have tried to turn off the serial function, and have it on. I've tried without the filter cap.

In all honesty, I'm not sure if this is my code or hardware... But I'll put my code here, it is about 900 lines.

/*
    Code Version 1.0
    WMH Racing Battery Wizard
    Written by Andrew Sarratore
    Date: 10/28/2023
    Code Version 1.1
    Add getCurrentAverage()
    add getFVoltageAverage()
    add getCalculations()
    add Program()
    Changes to the "Run Program" Menu
    Date:12/11/2023
    Code Version 1.2
    add getPVoltageAverage
    add more readings to the dishcharge display
    Code Version 1.3
    Date:12/27/2023
    add self calibrate VCC
    add mAH reading, untested
    add PID_v1.h library, still need to write PID code for the PWM value and replace current feedback loop
    Code Version 1.4
    Date:12/30/2023
    Add IR struct - Untested
    Add getIR function - still need to write the menu and page function
    Code Version 1.4.2
    Date:1/4/2024
    Calibrate initial QOV based on VCC during setup
    Fixed averages to be floats instead of int...
    Adding IR Reading menus
    Code Version 1.5
    PID added, Kp=0.25, Ki=10, Kd=0
    Added page 6 for displaying voltage data

    Other items still needed
    Data Return page
    Voltage values every 15 seconds - look into graphing this, would be cool.  
    
    
*/
    
#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"
#include "XPT2046_Touchscreen.h"
#include "Math.h"
#include "PID_v1.h"
#include "movingAvg.h"
 
// Define SPI pins for both display and touch
#define TFT_CS 10
#define TFT_DC 9
#define TFT_MOSI 11
#define TFT_CLK 13
#define TFT_RST 8
#define TFT_MISO 12
#define TS_CS 7
#define ROTATION 1
#define Isens A0
#define VFsens A1
#define VPsens A2
#define PWM 3
#define K (1.0/30)

char currentPage;
 

 
// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC/RST
Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);
XPT2046_Touchscreen ts(TS_CS);
 
// calibration values
float xCalM = -0.09, yCalM = -0.06; // gradients
float xCalC = 329.36, yCalC = 248.13; // y axis crossing points
 
int8_t blockWidth = 20; // block size
int8_t blockHeight = 20;
int16_t blockX = 0, blockY = 0; // block position (pixels)
 
class ScreenPoint {
public:
int16_t x;
int16_t y;
 
// default constructor
ScreenPoint(){
}
 
ScreenPoint(int16_t xIn, int16_t yIn){
x = xIn;
y = yIn;
}
};

class Button {
  public:

    int x;
    int y;
    int width;
    int height;
    char *text;

    Button(){
    }

    void initButtonG(int xPos, int yPos, int butWidth, int butHeight, char *butText){
      x = xPos;
      y = yPos;
      width = butWidth;
      height = butHeight;
      text = butText;
      renderG();
    }
    void initButtonR(int xPos, int yPos, int butWidth, int butHeight, char *butText){
      x = xPos;
      y = yPos;
      width = butWidth;
      height = butHeight;
      text = butText;
      renderR();
    }
     void initButtonB(int xPos, int yPos, int butWidth, int butHeight, char *butText){
      x = xPos;
      y = yPos;
      width = butWidth;
      height = butHeight;
      text = butText;
      renderB();
    }
    void initButtonY(int xPos, int yPos, int butWidth, int butHeight, char *butText){
      x = xPos;
      y = yPos;
      width = butWidth;
      height = butHeight;
      text = butText;
      renderY();
    }
    void renderG(){
      tft.fillRect(x,y,width,height,ILI9341_GREEN);
      tft.setCursor(x+5,y+5);
      tft.setTextSize(2);
      tft.setTextColor(ILI9341_WHITE);
      tft.print(text);
    }
    void renderR(){
      tft.fillRect(x,y,width,height,ILI9341_RED);
      tft.setCursor(x+5,y+5);
      tft.setTextSize(2);
      tft.setTextColor(ILI9341_WHITE);
      tft.print(text);
    }
    void renderB(){
      tft.fillRect(x,y,width,height,ILI9341_BLUE);
      tft.setCursor(x+5,y+5);
      tft.setTextSize(2);
      tft.setTextColor(ILI9341_WHITE);
      tft.print(text);
    }
    void renderY(){
      tft.fillRect(x,y,width,height,ILI9341_YELLOW);
      tft.setCursor(x+5,y+5);
      tft.setTextSize(2);
      tft.setTextColor(ILI9341_WHITE);
      tft.print(text);
    }
    bool isClicked(ScreenPoint sp) {
      if ((sp.x >= x) && (sp.x <= (x+width)) && (sp.y >= y) && (sp.y <= (y+height))){
        return true;
      }
      else {
        return false;
      }
    }
};

unsigned const int numChan = 25;
unsigned int chan;
struct ChannelData {
  float startVoltage;
  float endVoltage;
  float voltageDrop;
  float internalResistance;
};
ChannelData Channels[numChan]; 

//Touch Screen
ScreenPoint getScreenCoords(int16_t x, int16_t y){
int16_t xCoord = round((x * xCalM) + xCalC);
int16_t yCoord = round((y * yCalM) + yCalC);
if(xCoord < 0) xCoord = 0;
if(xCoord >= tft.width()) xCoord = tft.width() - 1;
if(yCoord < 0) yCoord = 0;
if(yCoord >= tft.height()) yCoord = tft.height() - 1;
return(ScreenPoint(xCoord, yCoord));
}


// I can probably make an array of the following button variables...
Button btnIP;// Current Selection +
Button btnIM;// Current Selection -
Button btnVP;// Cutoff V +
Button btnVM;// Cutoff V -
Button btnMI;// Main - Discharge Rate
Button btnMV;// Main - Voltage Cutoff 
Button btnMM;// Main Menu on other pages
Button btnCT;// Main Calibrate Touch
Button btnRP;// Main Menu - Run Program
Button btnPN;// Run/Running
Button btnMN;// Discharge - Main Menu
Button btnRN;// STOP
Button btnIR;// Main - IR Reading
Button btnRC;// IR Reading - Check IR
Button btnDD;// Discharge - dischargeData


double IV = 30; // Initial rate of discharge in ampres.  User changable in code
float VV = 350.00; // Cutoff voltage*100 so ++ works to change the value
unsigned long StartTime; // Start time to measure time of discharge
unsigned long CurrentTime; // Will give how long the discharge is running

//PID
double Input;
double Output;
//Specify the links and initial tuning parameters
double Kp=.01, Ki=15, Kd=0;
PID myPID(&Input, &Output, &IV, Kp, Ki, Kd, DIRECT);


// Current Variables
float VCC=0;// 4.967  measure the value(Measure value not needed with the VCC calibration code)
float VCCC=0;
float QOV=0; // Calibration happens in setup
float sens=0.04; //sensitivity of the current sensor
float iavg; // analogRead(Isens)/IN
float Icurrent=0; // Current Reading in Amps
float Icorrected=0; //iavg-IOFF
float voltageI=0; // iavg translated to voltage
float IOFF=0; // Offset to the raw read to zero current
int IN=75; // Sample number to average current
int mAH;
int rTmAH;
unsigned long runTime;

// Battery Voltage Variables (may be able to make a struct or class here as well)
int ValueR1F=5094;//Measured value
int ValueR2F=5104;//Measured value
int ValueR1P=5106;//Measured value
int ValueR2P=5082;//Measured value
int VFN=1000; // Sample number for average of analogRead(VFsens) Full voltage
int VPN=1000;// Sample number for average of analogRead(VPsens) Partial voltage
float vfavg; // Average of the analogRead(VFsens)/VFN      -Full Voltage
float vpavg; // Average of the analogRead(VPsens)/VPN      -Partial Voltage
float Fvoltage=0; // Calculation of FV seen by MCU
float Pvoltage=0; // Calculation of PV seen by MCU
float voltageFull=0;// Calculation of Full Voltage
float voltagePartial=0;// Calculation of Partial Voltage
float cell1=0; // Voltage of Cell 1
float cell2=0; // Voltage of Cell 2
//float avgLFV; // Leaky Average Full Voltage
//float LFvoltage;//Leaky Full Voltage
//float voltageLFull;// Leaky after divider
//float avgLPV;
//float LPvoltage;
//float voltageLPartial;
boolean isrunning=false;
boolean irRunning=false;
int zz=0;// variable used for analogWrite(PWM,zz)
float internalResistanceAVG=0;


//===================================================================SETUP=========================================================================================
void setup() {
Serial.begin(115200);
 
// avoid chip select contention
pinMode(TS_CS, OUTPUT);
digitalWrite(TS_CS, HIGH);
pinMode(TFT_CS, OUTPUT);
pinMode(Isens, INPUT);
pinMode(VFsens, INPUT);
pinMode(VPsens, INPUT);
pinMode(PWM, OUTPUT);
digitalWrite(TFT_CS, HIGH);

tft.begin();
tft.setRotation(ROTATION);
tft.fillScreen(ILI9341_BLACK);
ts.begin();
ts.setRotation(ROTATION);
//calibrateTouchScreen(); Leaving here for first run of the dispaly to get coordinates to add

getAccurateVoltage();
getCurrentAverage();
getFVoltageAverage();
getPVoltageAverage();
getCalculations();
calibrateQOV();

 

//initialize the variables we're linked to
  Input = Icurrent;
  //turn the PID on
  myPID.SetMode(AUTOMATIC);
currentPage = '0'; //  Indicates that we are at Home Screen
Home();
}

//unsigned long lastFrame = millis();

//==================================================================END SETUP=====================================================================


//======================================================================LOOP======================================================================
void loop() {
  ScreenPoint sp;
  // limit frame rate
//while((millis() - lastFrame) < 20); //(caused issues with millis function to get mAH, may need to revisit as this helped some flickering)
//lastFrame = millis();
getAccurateVoltage();
getCalculations();
//SerialData();

// Home Screen
if(currentPage=='0'){
    
if (ts.touched()) {
  TS_Point p = ts.getPoint();
  sp = getScreenCoords(p.x, p.y);
if(btnMV.isClicked(sp)){
    currentPage='1'; // Go to Discharge Current
     tft.fillScreen(ILI9341_BLACK);
     discharge();
   }
else if(btnMI.isClicked(sp)){
    currentPage='2'; // Go to Cutoff Voltage
     tft.fillScreen(ILI9341_BLACK);
     cutoff();
   }

   else if(btnCT.isClicked(sp)){
    currentPage='3'; // Go to Calibrate Screen
     tft.fillScreen(ILI9341_BLACK);
     calibrateTouchScreen();
   }

   else if(btnRP.isClicked(sp)){
    currentPage='4'; // Go to Run Program
     tft.fillScreen(ILI9341_BLACK);
     program();
   }

   else if(btnIR.isClicked(sp)){
     currentPage='5'; // Go to irReading
     tft.fillScreen(ILI9341_BLACK);
     irReading();
   }
  }
}


// Discharge Current
if(currentPage=='1'){
   tft.setCursor(100,150);
    tft.print(IV, 0);
    tft.setCursor(190,150);
    tft.print("A");
  if (ts.touched()) {
    TS_Point p = ts.getPoint();
    sp = getScreenCoords(p.x, p.y);
    if(btnIP.isClicked(sp)){
      if (IV<45){
      IV++;
      }
      tft.fillRect(100,150,125,50,ILI9341_BLACK);
      delay(100);
    }
    else if (btnIM.isClicked(sp)){
      IV--;
      tft.fillRect(100,150,125,50,ILI9341_BLACK);
     delay(100);
     
    }
    else if (btnMM.isClicked(sp)){
      currentPage='0';
      Home();
    }
  }
}// End Page 1

// Cutoff Voltage
if(currentPage=='2'){
   tft.setCursor(100,150);
   tft.print(VV/100);
   tft.setCursor(210,150);
   tft.print("V");
  if (ts.touched()) {
    TS_Point p = ts.getPoint();
    sp = getScreenCoords(p.x, p.y);
      if(btnIP.isClicked(sp)){
       VV++;
       tft.fillRect(100,150,100,50,ILI9341_BLACK);
       delay(50);
      }
      else if (btnIM.isClicked(sp)){
        if(VV>320){
       VV--;
        }
       tft.fillRect(100,150,100,50,ILI9341_BLACK);
       delay(50);
     }
      else if (btnMM.isClicked(sp)){
       currentPage='0';
       Home();
     }
  }
}// End Page 2

 
// Run Discharge (Surely I can move some of this to the page function, but when moved it caused the screen to refresh every loop)
if(currentPage=='4'){
    tft.setCursor(0,50);
    tft.setTextSize(2);
    tft.setTextColor(ILI9341_WHITE,ILI9341_BLACK);
    tft.print("Battery:");
    tft.setCursor(0,75);
    tft.print("Cutoff ");
    tft.print(VV/100);
    tft.print("V");
    tft.setTextSize(2);
    tft.setCursor(0,100);
    tft.print("Actual ");
    tft.print(voltageFull,2);
    tft.setCursor(145,100);
    tft.print("V");
    tft.setCursor(160,50);
    tft.print("Current:");
    tft.setCursor(160,75);
    tft.print("Rate ");
    tft.print(IV, 0);
    tft.print("A");
    tft.setCursor(160,100);
    tft.print("Actual ");
    tft.print(Icurrent,1);
    tft.setCursor(305,100);
    tft.print("A");
    tft.setTextSize(1);
    tft.setCursor(0,120);
    tft.print("Cell 1  ");
    tft.setCursor(45,120);
    tft.print(cell1,3);
    tft.setCursor(80,120);
    tft.print("V");
    tft.setCursor(0,130);
    tft.print("Cell 2  ");
    tft.setCursor(45,130);
    tft.print(cell2,3);
    tft.setCursor(80,130);
    tft.print("V");
    tft.setCursor(160,120);
    tft.print("Time");
    tft.setCursor(160,130);
    tft.fillRect(160,130,50,10,ILI9341_BLACK);
    tft.print(runTime);
    tft.setCursor(240,120);
    tft.print("mAH");
    tft.setCursor(240,130);
    tft.print(rTmAH);
    if (isrunning==true) {
      btnPN.initButtonR(0,15,140,25,"DISCHARGE");
      RunDischarge();
    }
       else
    {
      tft.setTextColor(ILI9341_WHITE,ILI9341_GREEN);
      btnPN.initButtonG(0,15,140,25,"    RUN");
    }
    if (ts.touched()) {
  TS_Point p = ts.getPoint();
  sp = getScreenCoords(p.x, p.y);
    if (btnMN.isClicked(sp)){
      currentPage='0';
      Home();
    }
    else if (btnRN.isClicked(sp)){
      zz=0;
      analogWrite(PWM, zz);
      isrunning=false;
    }
    else if (btnPN.isClicked(sp)){
     tft.fillRect(160,130,320,10,ILI9341_BLACK);
     rTmAH=0;
     runTime=0;
     StartTime=millis();
     isrunning=true;
        
    }
    else if (btnDD.isClicked(sp)){
      tft.fillScreen(ILI9341_BLACK);
      currentPage='6';
      dischargeData();
    }
    }   
}// End Page 4

// IR Reading
if(currentPage=='5'){
  tft.setCursor(0,120);
  tft.setTextSize(4);
  tft.setTextColor(ILI9341_WHITE,ILI9341_BLACK);
  tft.print(internalResistanceAVG);
  tft.setCursor(130,120);
  tft.print("m");
  tft.drawChar(155, 120, 233, ILI9341_WHITE,4,4);
  if (irRunning==true) {
      btnRC.initButtonR(90,15,140,25,"Reading IR");
      getIR();
  }
    else
  {
      tft.setTextColor(ILI9341_WHITE,ILI9341_GREEN);
      btnRC.initButtonG(90,15,140,25,"  Check IR  ");
  }
  if (ts.touched()) {
    TS_Point p = ts.getPoint();
    sp = getScreenCoords(p.x, p.y);
      if (btnMM.isClicked(sp)){
       currentPage='0';
       Home();
      }
        
  else if (btnRC.isClicked(sp)){
    irRunning=true;
  }
  }
}// End Page 5

if(currentPage=='6'){
  //dischargeData();
    if (ts.touched()) {
      TS_Point p = ts.getPoint();
      sp = getScreenCoords(p.x, p.y);
        if (btnMM.isClicked(sp)){
         currentPage='0';
         Home();
        }
    }
}// End Page 6

delay(0);
} 
//===========================================END LOOP=============================================
// Home Screen CurrentPage=0
void Home(){
    tft.fillScreen(ILI9341_BLACK);
    tft.setCursor(50,0);
    tft.setTextColor(0x07FF);
    tft.setTextSize(4);
    tft.print("MAIN MENU");
    tft.setTextColor(	0x07FF);
    tft.setTextSize(3);
    tft.setCursor(0,40);
    tft.print("1.");
    tft.setCursor(0,80);
    tft.print("2.");
    tft.setCursor(0,120);
    tft.print("3.");
    tft.setCursor(0,160);
    tft.print("4.");
    tft.setCursor(0,200);
    tft.print("5.");
    btnMV.initButtonG(50,40,200,25,"Discharge Rate");
    btnMI.initButtonG(50,80,200,25,"Voltage Cutoff");
    btnCT.initButtonG(50,120,200,25,"Calibrate Screen");
    btnRP.initButtonG(50,160,200,25,"Run Discharge");
    btnIR.initButtonG(50,200,200,25,"IR Reading");
    // add button for getIR function page, adjust button heights to fit, or make 2 colums and adjust width...
}
// Discharge Current Selection CurrentPage=1
void discharge(){
    btnIM.initButtonR(80,75,80,60,"-");
    btnIP.initButtonG(160,75,80,60,"+");
    btnMM.initButtonB(90,200,140,25," Main Menu");
    tft.setCursor(50,0);
    tft.setTextSize(4);
    tft.setTextColor(ILI9341_YELLOW);
    tft.print("DISCHARGE");
    tft.setCursor(70,40);
    tft.print("CURRENT");
    tft.setCursor(100,150);
    tft.print(IV, 0);
    tft.setCursor(190,150);
    tft.print("A");
}// End discharge

// Cutoff Voltage Selection CurrentPage=2
void cutoff(){
   
    btnIM.initButtonR(80,75,80,60,"-");
    btnIP.initButtonG(160,75,80,60,"+");
    btnMM.initButtonB(90,200,140,25," Main Menu");
    tft.setCursor(80,0);
    tft.setTextSize(4);
    tft.setTextColor(ILI9341_YELLOW);
    tft.print("VOLTAGE");
    tft.setCursor(95,40);
    tft.print("CUTOFF");
}// End Cutoff

// Run Program CurrentPage=4
void program(){
    btnPN.initButtonG(0,15,140,25,"    RUN");
    btnMN.initButtonB(90,200,140,25," Main Menu");
    btnRN.initButtonR(160,15,140,25,"   STOP   ");
    btnDD.initButtonY(90,150,140,25,"   DATA   ");
}// End Program

// IR Reading CurrentPage=5
void irReading(){
    btnRC.initButtonG(90,15,140,25,"  Check IR  ");
    btnMM.initButtonB(90,200,140,25," Main Menu");
}// End IR Reading

void dischargeData(){
    btnMM.initButtonB(90,200,140,25," Main Menu"); 
}// End dischargeData

void calibrateTouchScreen(){
TS_Point p;
int16_t x1,y1,x2,y2;
 
tft.fillScreen(ILI9341_BLACK);
// wait for no touch
while(ts.touched());
tft.drawFastHLine(10,20,20,ILI9341_RED);
tft.drawFastVLine(20,10,20,ILI9341_RED);
while(!ts.touched());
delay(50);
p = ts.getPoint();
x1 = p.x;
y1 = p.y;
tft.drawFastHLine(10,20,20,ILI9341_BLACK);
tft.drawFastVLine(20,10,20,ILI9341_BLACK);
delay(500);
while(ts.touched());
tft.drawFastHLine(tft.width() - 30,tft.height() - 20,20,ILI9341_RED);
tft.drawFastVLine(tft.width() - 20,tft.height() - 30,20,ILI9341_RED);
while(!ts.touched());
delay(50);
p = ts.getPoint();
x2 = p.x;
y2 = p.y;
tft.drawFastHLine(tft.width() - 30,tft.height() - 20,20,ILI9341_BLACK);
tft.drawFastVLine(tft.width() - 20,tft.height() - 30,20,ILI9341_BLACK);
 
int16_t xDist = tft.width() - 40;
int16_t yDist = tft.height() - 40;
 
// translate in form pos = m x val + c
// x
xCalM = (float)xDist / (float)(x2 - x1);
xCalC = 20.0 - ((float)x1 * xCalM);
// y
yCalM = (float)yDist / (float)(y2 - y1);
yCalC = 20.0 - ((float)y1 * yCalM);

currentPage='0';
Home();
/* // Serial print the actual coordinates from the touch calibrate, enter into the global variables, for first run of the screen
Serial.print("x1 = ");Serial.print(x1);
Serial.print(", y1 = ");Serial.print(y1);
Serial.print("x2 = ");Serial.print(x2);
Serial.print(", y2 = ");Serial.println(y2);
Serial.print("xCalM = ");Serial.print(xCalM);
Serial.print(", xCalC = ");Serial.print(xCalC);
Serial.print("yCalM = ");Serial.print(yCalM);
Serial.print(", yCalC = ");Serial.println(yCalC);
*/ 
}// END Calibrate




//  Current Average
void getCurrentAverage() {
  int ii; double rawIRead;
  for(int ii=0; ii<IN; ii++)
  {
    //analogRead(Isens);
    rawIRead+=analogRead(Isens);
  }
    iavg=rawIRead/IN;
    Icorrected=(iavg-IOFF);
}// end getCurrent

//   Full voltage average (2s voltage if a 2s battery or 1s voltage for a 1s battery)
  void getFVoltageAverage() {
  int jj; float rawVFRead;
  for(int jj=0; jj<VFN; jj++)
  {
    //analogRead(VFsens);
    rawVFRead+=analogRead(VFsens)+1;
  }
    vfavg=rawVFRead/VFN;
}// end getFVoltage

/*
void getLeakyFVoltage(){
  float sampF=analogRead(VFsens);
  avgLFV += K*(sampF-avgLFV);
}
*/

/*
void getLeakyPVoltage(){
  float sampP=analogRead(VPsens);
  avgLPV =+ K*(sampP-avgLPV);
}
*/

//  Partial Voltage average (only applies for 2s, this is cell 2 voltage)
void getPVoltageAverage() {
  int kk; float rawVPRead; 
  for(int kk=0; kk<VPN; kk++)
  {
   // analogRead(VPsens);
    rawVPRead+=analogRead(VPsens)+2;
  }
    vpavg=rawVPRead/VPN;
}//  end Partial Voltage

// Runs the main discharge function
void RunDischarge() {
  
  if (isrunning==true) {
    if (cell1>VV/100 && cell2>VV/100)
     {
        Input = Icurrent;
        myPID.Compute();
        analogWrite(PWM, Output);
      }
    
   else if (cell1<VV/100 || cell2<VV/100)
    {
      zz=0;
      analogWrite(PWM, zz);
     isrunning=false;

    }
   
  }

} // end runDischarge





void getCalculations() {
  
  getAccurateVoltage();
  getCurrentAverage();
  voltageI=Icorrected*(VCC/1023);
  Icurrent=abs((voltageI-QOV)/sens);
  getFVoltageAverage();
  Fvoltage=vfavg*(VCC/1023);
  voltageFull=((Fvoltage*(ValueR1F+ValueR2F))/ValueR2F);
  getPVoltageAverage();
  Pvoltage=vpavg*(VCC/1023);
  voltagePartial=((Pvoltage*(ValueR1P+ValueR2P))/ValueR2P);
  cell1=voltageFull-voltagePartial;
  if (isrunning==true) {
    cell2=voltagePartial;
  }
   else
  {
  cell2=voltagePartial;
  }
  VCC=VCCC/1000;
  
  if (isrunning==true) {
  CurrentTime=(millis()-StartTime)/1000;
  }
  if (CurrentTime>runTime) {
    runTime=CurrentTime;
  }
  else if (isrunning==false) {
    CurrentTime=0;// Need to change to keep the previous runtime around, but have a reset data button to zero time
  }
mAH=((Icurrent/3.6)*CurrentTime);
if (mAH>rTmAH) {
  rTmAH=mAH;
}
}// end Calculations

// calibrates the VCC using the bandgap ref - Something is amiss though, resoltion is 30mV instead of 3mV - need to work on this
void getAccurateVoltage() {
  int vv; float rawgetVoltage=0;
  for (vv=0; vv<10; vv++)
  {
    getVoltage();
    rawgetVoltage+=getVoltage();
  }
  VCCC = rawgetVoltage/10;
}
// Read the voltage of the battery the Arduino is currently running on (in millivolts)
float getVoltage() {
    const long InternalReferenceVoltage = 1080; // Adjust this value to your boards specific internal BG voltage x1000
    ADMUX = (0<<REFS1) | (1<<REFS0) | (0<<ADLAR) | (1<<MUX3) | (1<<MUX2) | (1<<MUX1) | (0<<MUX0);
    ADCSRA |= _BV( ADSC ); // Start a conversion 
    while( ( (ADCSRA & (1<<ADSC)) != 0 ) ); // Wait for it to complete
    float results = (((InternalReferenceVoltage * 1024) / ADC) + 5); // Scale the value; calculates for straight line value
    return results; 
}// end VCC calibration


//Internal Resistance Measurement (This is untested and need menu and page written)
void getIR() {
 if (isrunning==false) {
   irRunning=true;
   for (int chan=0; chan<(numChan-1); chan++)
   {
     tft.fillRect(0,120,130,40,ILI9341_BLACK);
    getCalculations();
    Channels[chan].startVoltage=voltageFull; // record unloaded voltage of battery
    zz=50; // Will be appx 20A with 2s, may split this into if statements based on current battery voltage
    analogWrite(PWM, zz);//Turn Mosfets on
    delay(0);//delay to stabalize - may need to fine tune this delay, but the shorter the better
    getCalculations();
    Channels[chan].endVoltage=voltageFull; // record voltage of battery under load
    zz=0;//
    analogWrite(PWM, zz);//Turn off the mosfets
    Channels[chan].voltageDrop=Channels[chan].startVoltage-Channels[chan].endVoltage;// Voltage drop from the load
    Channels[chan].internalResistance=(Channels[chan].voltageDrop/Icurrent)*1000;//Ohms Law V=IR, R=V/I, Readings in mΩ
    delay(0);// Allow for stablization between readings - may need to fine tune this delay, but start on the high side
   }
    for (int chan=0; chan<(numChan-1); chan++) {
   internalResistanceAVG=+Channels[chan].internalResistance;// run the readings numChan times and average the IR
  }
 }//End if
 irRunning=false;
}//end getIR

void calibrateQOV() {
  analogRead(Isens);
  QOV=(analogRead(Isens)*VCC)/1023;
}


void SerialData() {
  getCalculations();
  Serial.println("----------------------------------------");
  Serial.println();
  Serial.print(VCC);
  Serial.print("V VCC ");
  Serial.print(QOV);
  Serial.print(" QOV ");
  Serial.println();
  Serial.print(analogRead(Isens));
  Serial.print(" RAW Read ");
  Serial.print(Icorrected);
  Serial.print(" Corrected ");
  Serial.print(voltageI);
  Serial.print("V ");
  Serial.print(Icurrent);
  Serial.print(" Amps ");
  Serial.print(iavg);
  Serial.print(" AVG ");
  Serial.print(Output);
  Serial.println();
  Serial.print(voltageFull);
  Serial.print(" V Full  ");
  Serial.print(vfavg);
  Serial.print("  ACT  ");
  Serial.print(analogRead(VFsens));
  Serial.print(" ");
  Serial.println();
  Serial.print(voltagePartial);
  Serial.print("   ");
  Serial.print(" V Partial   ");
  Serial.print(vpavg);
  Serial.print("   ACT  ");
  Serial.print(analogRead(VPsens));
  Serial.println();
  Serial.print("Cell 1 ");
  Serial.print(cell1,3);
  Serial.print("V   Cell 2 ");
  Serial.print(cell2,3);
  Serial.print("V");
  Serial.println();
 }




Seems you have proven that all batteries have internal resistance. The older and more used the battery is, the higher the internal resistance.

Well, yes that is another thing this unit measures. But that isn't the case here. Yes there is internal resistance, but look at the pics I put up in my second post. The under load voltage is off, not just lower like it should be. My unit gives the wrong voltage.

Are you using bare calls/batteries or do they have some type of management devices included?

Exactly. That is due to the battery and wiring internal resistance.

Ok on the first, I get the /1023 and /1024.

On the second, I have measure VCC under load and it is stable. The load doesn't interfere with the voltage regulator. Also, I am using the internal bandgap to calibrate VCC in the code, so if VCC changes then the calculations should change.

I am using a 4 wire method here. The wires that are connected to the battery for voltage are not the same paths used to deliver the load. So wire resistance shouldn't be an issue.

The problem isn't the voltage drop under load, the problem is it is measuring it incorrectly compared to my DMM. Which I'll trust my DMM over my code.

It isn't on a timer, just happens every time through the loop.

1 Like

The 4 wire method cannot compensate for voltage drop due to battery internal resistance.

Please post a hand drawn schematic diagram of the entire test rig, so we have a clear idea what you are talking about.

No need for a hand drawn.

My routing could use work... But here is the layout.

That is part of the system schematic.

AREF requires a decoupling cap. Without it, you can expect excessive noise in the ADC readings.

Well that is one I have missed on all my designs then. Even when aref is VCC?

I think I may have come up with a code way to fix it. Like I said, this is only under load and it seems to be proportional to the load current.

So this is what I placed in my code and my dmm agrees with the results so far.

 
 if (isrunning==true){
  voltagePartial=((Pvoltage*(ValueR1P+ValueR2P))/ValueR2P)+((IV/1000)*4);
  voltageFull=((Fvoltage*(ValueR1F+ValueR2F))/ValueR2F)+((IV/1000)*4);
  cell1=voltageFull-voltagePartial;
  cell2=voltagePartial;
}
 else
{
 voltagePartial=((Pvoltage*(ValueR1P+ValueR2P))/ValueR2P);
 voltageFull=((Fvoltage*(ValueR1F+ValueR2F))/ValueR2F);
  cell1=voltageFull-voltagePartial;
  cell2=voltagePartial;
}

The good news is the aref pin and gnd are next to each other on my chip, so I can just put a .1uF cap on those pins pretty easily.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.