Correction: capacityAh should be 90 not the value from the BMU.
Correction: capacityAh should be 90 not the value from the BMU..
MickeyS70 said:It probably needs to be a variable at a later stage when taking temperature compensation, allowable voltage range, SOH and whatever else affects it, into account?
That’s probably true for the improved LEV50Ns as my original LEV50 pack certainly performs a lot worse in winter.Hopefully the capacity doesn't change much. The temperature effect is not large. According to the Yuasa tech report on the LEV50 at -25oC the Ah capacity is 90% of the +25oC Ah capacity. I think that piev is right when he wrote that monitoring the voltage is sufficient to avoid problems. It should be enough just to change the 90 Ah to a new lower value as the cells age. This may be necessary once a month or once a year depending on how much the car is used.
That’s already been tried, while the general consensus was that it should work, in practice it didn’t for reasons unknown…Or maybe approach it from the other side. Instead of cheating the capacity, cheat the coulomb counter. Or maybe you could even physically shunt?
Sorry, I don't know English (Google translate)
I think you should use the battery's internal resistance value.Computing the SoC from the voltage is quick and easy solution and it is a step in the right direction. But it doesn't work well unless the amps are between -1 and 1 for at least some minutes. When the battery is under load the SoC computed from the voltage can be as much as 20 % points below the true SoC.
That’s an interesting idea Frud, I incorporated coulomb counting with voltage used as a a remaining amp hour indicator. It seems to work very well. I still need to do some testing but I’m very pleased with the results.I think you should use the battery's internal resistance value.
For example, if the battery voltage without load is 352V(4*88), the battery resistance is 66mOhm. If you give a load of 100A, then the voltage drop across the batteries will be 6.6V, the total voltage will be 352-6.6 = 345.4V.
Thus, knowing the load current, the total battery voltage and the internal resistance of the battery, you can determine the no-load voltage, relative to which the SOC should be calculated.
SOC 345.4|100A = SOC 352|0A
Interesting take, but unfortunately cell internal resistance isn’t a constant either; it is influenced by various factors, most importantly temperature.I think you should use the battery's internal resistance value.
For example, if the battery voltage without load is 352V(4*88), the battery resistance is 66mOhm. If you give a load of 100A, then the voltage drop across the batteries will be 6.6V, the total voltage will be 352-6.6 = 345.4V.
Thus, knowing the load current, the total battery voltage and the internal resistance of the battery, you can determine the no-load voltage, relative to which the SOC should be calculated.
Could I see your code? I have a lot of experience programming Arduino. Perhaps I could optimize the code for greater speed and performance.That’s an interesting idea Frud, I incorporated coulomb counting with voltage used as a a remaining amp hour indicator. It seems to work very well. I still need to do some testing but I’m very pleased with the results.
Could I see your code? I have a lot of experience programming Arduino. Perhaps I could optimize the code for greater speed and performance.That’s an interesting idea Frud, I incorporated coulomb counting with voltage used as a a remaining amp hour indicator. It seems to work very well. I still need to do some testing but I’m very pleased with the results.
// Arduino Due - Displays all traffic found on either canbus port
// By Thibaut Viard/Wilfredo Molina/Collin Kidder 2013-2014
// Modified by Paul Dove/ David Cecil to make a CAN bridge between ECU and BMU in 2012 Mitsubishi i-MiEV
// Reads all traffic on CAN0 and forwards it to CAN1 (and in the reverse direction)
// but modifies Soc1, SoC2 and Capacity values first PID 0x374 Bytes [0], [1], and [6].
// Required libraries
#include "variant.h"
#include <due_can.h>
#include <SPI.h>
//#include <SdFat.h> not used uncomment if using SD shield
//const int chipSelect = 4; //not used scared for SD card on Ethernet shield
//Leave defined if you use Due native port, comment if using programming port
#define Serial SerialUSB
//SdFat sd;//Not used at this time
//SdFile myFile;// scared in for writing to SD card using Ethernet shield
//Global variables
float volts; //Average cell voltage
byte ah; //Battery Voltage high byte PID 0x373 byte[4]
byte al; //Battery Voltage low byte PID 0x373 byte[5]
float amps; //Battery Curent
float remAh; //Capacity remaining in battery
double capAh = 90; //Battery Capacity
float SoCv; //State of Charge based on battery voltage
float SoCah; //State of Charge based on Coulomb counting
long presTime; //Time in milliseconds
float step; //Current interval
long prevTime = millis(); //time since last current calculation
int flag = 0; //power up flag
long lowMins; //Battery current at 0 timer
int j = 0; //counter for valid data
void setup() {
Serial.begin(115200); //Initialize Serial port on Arduino 115,200 Baud
// Initialize CAN0 and CAN1, Set the proper baud rates here
Can0.begin(CAN_BPS_500K);
Can1.begin(CAN_BPS_500K);
//Begin looking for data on Can0 and Can1
Can0.watchFor(); //class CANRaw instance watchfor()
Can1.watchFor(); //class CANRaw instance watchfor()
}
//subroutine to store SoC based on voltage of the battery
void storeSoC() {
if (volts < 3.05376) {
SoCv = 0;return;
} else if (volts < 3.54912) {
SoCv = 12.0962747344626 * volts - 36.9391199331124;return;
} else if (volts < 3.65472) {
SoCv = 60.5253601258929 * volts - 208.819755477546;return;
} else if (volts < 3.70464) {
SoCv = 122.699648332138 * volts - 436.049370070673;return;
} else if (volts < 3.77376) {
SoCv = 138.703950288505 * volts - 495.33954727031;return;
} else if (volts < 3.7776) {
SoCv = 1629.7714158898 * volts - 6119.20772303549;return;
} else if (volts < 3.78528) {
SoCv = 814.885707944995 * volts - 3040.89547270319;return;
} else if (volts < 3.79488) {
SoCv = 665.778961384803 * volts - 2476.48468708382;return;
} else if (volts < 3.81216) {
SoCv = 362.171425753317 * volts - 1324.33052226661;return;
} else if (volts < 3.83136) {
SoCv = 325.95428317799 * volts - 1186.26498002665;return;
} else if (volts < 3.86592) {
SoCv = 181.085712876656 * volts - 631.221334516933;return;
} else if (volts < 3.91584) {
SoCv = 122.699648332139 * volts - 405.505479872993;return;
} else if (volts < 3.95232) {
SoCv = 167.904781928189 * volts - 582.521550213749;return;
} else if (volts < 3.99264) {
SoCv = 158.518800329719 * volts - 545.425147422485;return;
} else if (volts < 4.04064) {
SoCv = 130.381713271193 * volts - 433.083888149129;return;
} else if (volts < 4.09248) {
SoCv = 120.72380858444 * volts - 394.05977215565;return;
} else if (volts < 4.14816) {
SoCv = 117.180923519597 * volts - 379.560585885481;return;
} else if (volts < 4.20384) {
SoCv = 112.398028682064 * volts - 359.720372836221;return;
} else {SoCv = 112.782956058589;return;}
}
//Main program loop
void loop(){
CAN_FRAME incoming; //capture can frames
if (Can0.available() > 0) { //if there is an incoming frame read it
Can0.read(incoming);
if (incoming.id == 0x373){ //save BMU reported voltage and current in variable volts
byte vh = incoming.data.byte[4];
byte vl = incoming.data.byte[5];
volts = (vh * 256 + vl) / 10.0 / 88.0;//4.1
if (flag == 0){ //check to see if this is a new power up
if (j == 20){ //wait 20 frames until CMU is providing good data
storeSoC(); //get battery SoC based on voltage
remAh = (SoCv/100)*capAh; //calculate the capacity remaining in the battery based on voltage
//Serial.print("remAh1 = "); Serial.println(remAh);
//Serial.print("SoCv = ");Serial.println(SoCv);
flag = 1;
} j=j+1;
}
byte ah = incoming.data.byte[2]; //store current bytes and calculate battery current
byte al = incoming.data.byte[3];
amps = (ah * 256 - al -32768)/100.0;
//Serial.print ("volts = "); Serial.println (volts);
//Serial.print ("amps = "); Serial.println (amps);
presTime = millis(); //calculate the interval for amp hour calculation
step = (presTime - prevTime)/1000.0/3600.0;
prevTime = presTime;
remAh = remAh + amps*step; //update remaining amp hours based on current in or out of battery since startup
if (amps < 1 && amps > -1) { //store minutes elapsed since current was 0
lowMins += step * 60;
} else {lowMins = 0;}
//Serial.print("remAh2 ="); Serial.println(remAh);
Can1.sendFrame(incoming); //send can frame out to ECU
}
if (incoming.id == 0x374){ //capture SoC and Capacity from BMU
incoming.data.byte[6] = 0xB4; //modify capacity to new capacity of 90 Ah
if (lowMins > 3) { //when battery current has been 0 for long enough for battery voltage to settle
storeSoC(); //get SoC based on battery voltage
remAh = (SoCv/100)*capAh; //correct remaining capacity bsed on voltage
//Serial.print("remAh3 = "); Serial.println(remAh);
}
else { lowMins < 10; //wait if current has not been 0 long enough
SoCv = 100.0 * remAh / capAh;
}
//Serial.println(remAh);
//Serial.print("SoCv = ");Serial.println(SoCv);
incoming.data.byte[0] = 2 * SoCv + 10; //modify data coming from BMU
incoming.data.byte[1] = 2 * SoCv + 10;
Can1.sendFrame(incoming); //send corrected SoC based on 90 Amp hours to ECU
}
else Can1.sendFrame(incoming); //send any captured frame from BMU that was not 0x373 or 0x374 to ECU
}
if (Can1.available() > 0) { //look for messages from ECU and forward to BMU
Can1.read(incoming);
Can0.sendFrame(incoming);
}
}
Code:// Arduino Due - Displays all traffic found on either canbus port // By Thibaut Viard/Wilfredo Molina/Collin Kidder 2013-2014 // Modified by Paul Dove/ David Cecil to make a CAN bridge between ECU and BMU in 2012 Mitsubishi i-MiEV // Reads all traffic on CAN0 and forwards it to CAN1 (and in the reverse direction) // but modifies Soc1, SoC2 and Capacity values first PID 0x374 Bytes [0], [1], and [6]. // Required libraries #include "variant.h" #include <due_can.h> #include <SPI.h> //#include <SdFat.h> not used uncomment if using SD shield //const int chipSelect = 4; //not used scared for SD card on Ethernet shield //Leave defined if you use Due native port, comment if using programming port #define Serial SerialUSB //SdFat sd;//Not used at this time //SdFile myFile;// scared in for writing to SD card using Ethernet shield //Global variables float volts; //Average cell voltage byte ah; //Battery Voltage high byte PID 0x373 byte[4] byte al; //Battery Voltage low byte PID 0x373 byte[5] float amps; //Battery Curent float remAh; //Capacity remaining in battery double capAh = 90; //Battery Capacity float SoCv; //State of Charge based on battery voltage float SoCah; //State of Charge based on Coulomb counting long presTime; //Time in milliseconds float step; //Current interval long prevTime = millis(); //time since last current calculation int flag = 0; //power up flag long lowMins; //Battery current at 0 timer int j = 0; //counter for valid data void setup() { Serial.begin(115200); //Initialize Serial port on Arduino 115,200 Baud // Initialize CAN0 and CAN1, Set the proper baud rates here Can0.begin(CAN_BPS_500K); Can1.begin(CAN_BPS_500K); //Begin looking for data on Can0 and Can1 Can0.watchFor(); //class CANRaw instance watchfor() Can1.watchFor(); //class CANRaw instance watchfor() } //subroutine to store SoC based on voltage of the battery void storeSoC() { if (volts < 3.05376) { SoCv = 0;return; } else if (volts < 3.54912) { SoCv = 12.0962747344626 * volts - 36.9391199331124;return; } else if (volts < 3.65472) { SoCv = 60.5253601258929 * volts - 208.819755477546;return; } else if (volts < 3.70464) { SoCv = 122.699648332138 * volts - 436.049370070673;return; } else if (volts < 3.77376) { SoCv = 138.703950288505 * volts - 495.33954727031;return; } else if (volts < 3.7776) { SoCv = 1629.7714158898 * volts - 6119.20772303549;return; } else if (volts < 3.78528) { SoCv = 814.885707944995 * volts - 3040.89547270319;return; } else if (volts < 3.79488) { SoCv = 665.778961384803 * volts - 2476.48468708382;return; } else if (volts < 3.81216) { SoCv = 362.171425753317 * volts - 1324.33052226661;return; } else if (volts < 3.83136) { SoCv = 325.95428317799 * volts - 1186.26498002665;return; } else if (volts < 3.86592) { SoCv = 181.085712876656 * volts - 631.221334516933;return; } else if (volts < 3.91584) { SoCv = 122.699648332139 * volts - 405.505479872993;return; } else if (volts < 3.95232) { SoCv = 167.904781928189 * volts - 582.521550213749;return; } else if (volts < 3.99264) { SoCv = 158.518800329719 * volts - 545.425147422485;return; } else if (volts < 4.04064) { SoCv = 130.381713271193 * volts - 433.083888149129;return; } else if (volts < 4.09248) { SoCv = 120.72380858444 * volts - 394.05977215565;return; } else if (volts < 4.14816) { SoCv = 117.180923519597 * volts - 379.560585885481;return; } else if (volts < 4.20384) { SoCv = 112.398028682064 * volts - 359.720372836221;return; } else {SoCv = 112.782956058589;return;} } //Main program loop void loop(){ CAN_FRAME incoming; //capture can frames if (Can0.available() > 0) { //if there is an incoming frame read it Can0.read(incoming); if (incoming.id == 0x373){ //save BMU reported voltage and current in variable volts byte vh = incoming.data.byte[4]; byte vl = incoming.data.byte[5]; volts = (vh * 256 + vl) / 10.0 / 88.0;//4.1 if (flag == 0){ //check to see if this is a new power up if (j == 20){ //wait 20 frames until CMU is providing good data storeSoC(); //get battery SoC based on voltage remAh = (SoCv/100)*capAh; //calculate the capacity remaining in the battery based on voltage //Serial.print("remAh1 = "); Serial.println(remAh); //Serial.print("SoCv = ");Serial.println(SoCv); flag = 1; } j=j+1; } byte ah = incoming.data.byte[2]; //store current bytes and calculate battery current byte al = incoming.data.byte[3]; amps = (ah * 256 - al -32768)/100.0; //Serial.print ("volts = "); Serial.println (volts); //Serial.print ("amps = "); Serial.println (amps); presTime = millis(); //calculate the interval for amp hour calculation step = (presTime - prevTime)/1000.0/3600.0; prevTime = presTime; remAh = remAh + amps*step; //update remaining amp hours based on current in or out of battery since startup if (amps < 1 && amps > -1) { //store minutes elapsed since current was 0 lowMins += step * 60; } else {lowMins = 0;} //Serial.print("remAh2 ="); Serial.println(remAh); Can1.sendFrame(incoming); //send can frame out to ECU } if (incoming.id == 0x374){ //capture SoC and Capacity from BMU incoming.data.byte[6] = 0xB4; //modify capacity to new capacity of 90 Ah if (lowMins > 3) { //когда ток батареи равен 0 достаточно долго, чтобы напряжение батареи стабилизировалось магазинSoC(); //получить SoC на основе напряжения батареи ремАч = (SoCv/100)*емкость Ач; //корректируем оставшуюся емкость в зависимости от напряжения //Serial.print("reMAH3 = "); Serial.println(рема); } еще { lowMins <10; //ждём, если текущее значение не было равно 0 достаточно долго SoCv = 100,0 * remah/capAh; } //Serial.println(рема); //Serial.print("SoCv = ");Serial.println(SoCv); incoming.data.byte[0] = 2 * SoCv + 10; //изменяем данные, поступающие от BMU incoming.data.byte[1] = 2 * SoCv + 10; Can1.sendFrame(входящий); //отправляем в ECU исправленный SoC на основе 90 ампер-часов } еще Can1.sendFrame(входящий); // отправляем любой захваченный кадр из BMU, который не был 0x373 или 0x374, в ECU } if (Can1.available() > 0) { //ищем сообщения от ECU и пересылаем их в BMU Can1.read(входящий); Can0.sendFrame(входящий); } }[/КОД] [/QUOTE]
// Arduino Due - Displays all traffic found on either canbus port
// By Thibaut Viard/Wilfredo Molina/Collin Kidder 2013-2014
// Modified by Paul Dove/ David Cecil to make a CAN bridge between ECU and BMU in 2012 Mitsubishi i-MiEV
// Reads all traffic on CAN0 and forwards it to CAN1 (and in the reverse direction)
// but modifies Soc1, SoC2 and Capacity values first PID 0x374 Bytes [0], [1], and [6].
// Required libraries
#include "variant.h"
#include <due_can.h>
#include <SPI.h>
//#include <SdFat.h> not used uncomment if using SD shield
//const int chipSelect = 4; //not used scared for SD card on Ethernet shield
//Leave defined if you use Due native port, comment if using programming port
#define Serial SerialUSB
//SdFat sd;//Not used at this time
//SdFile myFile;// scared in for writing to SD card using Ethernet shield
//Global variables
float volts; //Average cell voltage
byte ah; //Battery Voltage high byte PID 0x373 byte[4]
byte al; //Battery Voltage low byte PID 0x373 byte[5]
float amps; //Battery Curent
float remAh; //Capacity remaining in battery
double capAh = 90; //Battery Capacity
float SoCv; //State of Charge based on battery voltage
float SoCah; //State of Charge based on Coulomb counting
long presTime; //Time in milliseconds
float step; //Current interval
long prevTime = millis(); //time since last current calculation
int flag = 0; //power up flag
long lowMins; //Battery current at 0 timer
int j = 0; //counter for valid data
void setup() {
Serial.begin(115200); //Initialize Serial port on Arduino 115,200 Baud
// Initialize CAN0 and CAN1, Set the proper baud rates here
Can0.begin(CAN_BPS_500K);
Can1.begin(CAN_BPS_500K);
//Begin looking for data on Can0 and Can1
Can0.watchFor(); //class CANRaw instance watchfor()
Can1.watchFor(); //class CANRaw instance watchfor()
}
//subroutine to store SoC based on voltage of the battery
void storeSoC(){
uint8_t i = 0;
uint16_t Vbat = 0;
uint16_t Vsoc[] = {2687,3051,3181,3235,3269,3301,3321,3323,3327,3333,3340,3351,3364,3383,3410,3446,3473,3500,3531,3565,3601};//
Vbat = uint16_t(volts);
while((Vbat >= Vsoc[i] && Vbat <= Vsoc[i+1]) != 1){
i++;
}
if(Vbat >= Vsoc[0]){
SoCv = float(map(Vbat, Vsoc[i], Vsoc[i+1], i*5, (i+1)*5));
}else{
SoCv = 0;
}
i = 0;
}
//Main program loop
void loop(){
CAN_FRAME incoming; //capture can frames
if (Can0.available() > 0) { //if there is an incoming frame read it
Can0.read(incoming);
if (incoming.id == 0x373){ //save BMU reported voltage and current in variable volts
byte vh = incoming.data.byte[4];
byte vl = incoming.data.byte[5];
volts = (vh * 256 + vl);//4.1
if (flag == 0){ //check to see if this is a new power up
if (j == 20){ //wait 20 frames until CMU is providing good data
storeSoC(); //get battery SoC based on voltage
remAh = (SoCv/100)*capAh; //calculate the capacity remaining in the battery based on voltage
//Serial.print("remAh1 = "); Serial.println(remAh);
//Serial.print("SoCv = ");Serial.println(SoCv);
flag = 1;
} j=j+1;
}
byte ah = incoming.data.byte[2]; //store current bytes and calculate battery current
byte al = incoming.data.byte[3];
amps = (ah * 256 - al -32768)/100.0;
//Serial.print ("volts = "); Serial.println (volts);
//Serial.print ("amps = "); Serial.println (amps);
presTime = millis(); //calculate the interval for amp hour calculation
step = (presTime - prevTime)/1000.0/3600.0;
prevTime = presTime;
remAh = remAh + amps*step; //update remaining amp hours based on current in or out of battery since startup
if (amps < 1 && amps > -1) { //store minutes elapsed since current was 0
lowMins += step * 60;
} else {lowMins = 0;}
//Serial.print("remAh2 ="); Serial.println(remAh);
Can1.sendFrame(incoming); //send can frame out to ECU
}
if (incoming.id == 0x374){ //capture SoC and Capacity from BMU
incoming.data.byte[6] = 0xB4; //modify capacity to new capacity of 90 Ah
if (lowMins > 3) { //when battery current has been 0 for long enough for battery voltage to settle
storeSoC(); //get SoC based on battery voltage
remAh = (SoCv/100)*capAh; //correct remaining capacity bsed on voltage
//Serial.print("remAh3 = "); Serial.println(remAh);
}
else { lowMins < 10; //wait if current has not been 0 long enough
SoCv = 100.0 * remAh / capAh;
}
//Serial.println(remAh);
//Serial.print("SoCv = ");Serial.println(SoCv);
incoming.data.byte[0] = 2 * SoCv + 10; //modify data coming from BMU
incoming.data.byte[1] = 2 * SoCv + 10;
Can1.sendFrame(incoming); //send corrected SoC based on 90 Amp hours to ECU
}
else Can1.sendFrame(incoming); //send any captured frame from BMU that was not 0x373 or 0x374 to ECU
}
if (Can1.available() > 0) { //look for messages from ECU and forward to BMU
Can1.read(incoming);
Can0.sendFrame(incoming);
}
}
Frud said:I changed the code of the storeSoC() function.
could you test it?
Frud said:I think you should use the battery's internal resistance value.
Frud said:I don’t know why you calculate the average cell voltage.
Frud said:For programming, it is better to use an integer data type.
I'm using a 2015 Zoe NMC pack in my solar storage and specifically got a 3rd party BMS capable of actively balancing the cells. I disabled the function to see how much they drift over time; could have saved that money, the cells are still within the same 3mV after a year of daily use.This has got me to thinking about how to convert as much as possible in the code to integer arithmetic. It's a good idea but right now it is best to limit the changes until we have something running dependably.
However there is one thing I'm wondering about. In PID 0x373 byte 0 is the the highest cell voltage and byte 1 is the the lowest cell voltage. If we use these to compute the SoC based on voltage we can avoid the problem of 80 cell and 88 cell cars. We can compute the average of the highest and lowest cells and this will be very close to the average of all the cells. On the other hand the lowest voltage may in fact be the right value to use. As the cells are in series then the maximum Ah that can pass through the battery is equal to the Ah in the weakest cell and this is the cell with the lowest voltage. Any thoughts anybody?
Enter your email address to join: