diff --git a/.github/workflows/compile-all-batteries.yml b/.github/workflows/compile-all-batteries.yml index 7e2febc5..8309a209 100644 --- a/.github/workflows/compile-all-batteries.yml +++ b/.github/workflows/compile-all-batteries.yml @@ -41,6 +41,7 @@ jobs: - RENAULT_KANGOO_BATTERY - RENAULT_ZOE_BATTERY - TESLA_MODEL_3_BATTERY + - VOLVO_SPA_BATTERY - TEST_FAKE_BATTERY # These are the emulated inverter communication protocols for which the code will be compiled. inverter: diff --git a/.github/workflows/compile-all-combinations.yml b/.github/workflows/compile-all-combinations.yml index e2c24be9..dfa15484 100644 --- a/.github/workflows/compile-all-combinations.yml +++ b/.github/workflows/compile-all-combinations.yml @@ -44,6 +44,7 @@ jobs: - RENAULT_KANGOO_BATTERY - RENAULT_ZOE_BATTERY - TESLA_MODEL_3_BATTERY + - VOLVO_SPA_BATTERY - TEST_FAKE_BATTERY # These are the emulated inverter communication protocols for which the code will be compiled. inverter: diff --git a/Software/USER_SETTINGS.h b/Software/USER_SETTINGS.h index 09dba6c4..021dd056 100644 --- a/Software/USER_SETTINGS.h +++ b/Software/USER_SETTINGS.h @@ -17,6 +17,7 @@ //#define RENAULT_ZOE_BATTERY //#define SANTA_FE_PHEV_BATTERY //#define TESLA_MODEL_3_BATTERY +//#define VOLVO_SPA_BATTERY //#define TEST_FAKE_BATTERY /* Select inverter communication protocol. See Wiki for which to use with your inverter: https://github.com/dalathegreat/BYD-Battery-Emulator-For-Gen24/wiki */ diff --git a/Software/src/battery/BATTERIES.h b/Software/src/battery/BATTERIES.h index dfef51e3..0c1c141b 100644 --- a/Software/src/battery/BATTERIES.h +++ b/Software/src/battery/BATTERIES.h @@ -43,6 +43,10 @@ #include "TEST-FAKE-BATTERY.h" //See this file for more Fake battery settings #endif +#ifdef VOLVO_SPA_BATTERY +#include "VOLVO-SPA-BATTERY.h" //See this file for more XC40 Recharge/Polestar2 settings +#endif + #ifdef SERIAL_LINK_RECEIVER #include "SERIAL-LINK-RECEIVER-FROM-BATTERY.h" //See this file for more Serial-battery settings #endif diff --git a/Software/src/battery/VOLVO-SPA-BATTERY.cpp b/Software/src/battery/VOLVO-SPA-BATTERY.cpp new file mode 100644 index 00000000..d7c6532b --- /dev/null +++ b/Software/src/battery/VOLVO-SPA-BATTERY.cpp @@ -0,0 +1,388 @@ +#ifdef VOLVO_SPA_BATTERY +#include "VOLVO-SPA-BATTERY.h" +#include "../devboard/utils/events.h" +#include "../lib/miwagner-ESP32-Arduino-CAN/CAN_config.h" +#include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" + +/* Do not change code below unless you are sure what you are doing */ +static unsigned long previousMillis100 = 0; // will store last time a 100ms CAN Message was send +static unsigned long previousMillis60s = 0; // will store last time a 60s CAN Message was send +static const int interval100 = 100; // interval (ms) at which send CAN Messages +static const int interval60s = 60000; // interval (ms) at which send CAN Messages +static uint8_t CANstillAlive = 12; //counter for checking if CAN is still alive + +#define MAX_CELL_VOLTAGE 4210 //Battery is put into emergency stop if one cell goes over this value +#define MIN_CELL_VOLTAGE 2700 //Battery is put into emergency stop if one cell goes below this value +#define MAX_CELL_DEVIATION 500 //LED turns yellow on the board if mv delta exceeds this value + +static float BATT_U = 0; //0x3A +static float MAX_U = 0; //0x3A +static float MIN_U = 0; //0x3A +static float BATT_I = 0; //0x3A +static int32_t CHARGE_ENERGY = 0; //0x1A1 +static uint8_t BATT_ERR_INDICATION = 0; //0x413 +static float BATT_T_MAX = 0; //0x413 +static float BATT_T_MIN = 0; //0x413 +static float BATT_T_AVG = 0; //0x413 +static uint16_t SOC_BMS = 0; //0X37D +static uint16_t SOC_CALC = 0; +static uint16_t CELL_U_MAX = 0; //0x37D +static uint16_t CELL_U_MIN = 0; //0x37D +static uint8_t CELL_ID_U_MAX = 0; //0x37D +static uint16_t HvBattPwrLimDchaSoft = 0; //0x369 + +static uint8_t batteryModuleNumber = 0x10; // First battery module +static uint8_t battery_request_idx = 0; +static uint8_t rxConsecutiveFrames = 0; +static uint16_t min_max_voltage[2]; //contains cell min[0] and max[1] values in mV +static uint16_t cell_deviation_mV = 0; //contains the deviation between highest and lowest cell in mV +static uint8_t cellcounter = 0; +static uint32_t remaining_capacity = 0; +static uint16_t cell_voltages[108]; //array with all the cellvoltages + +CAN_frame_t VOLVO_536 = {.FIR = {.B = + { + .DLC = 8, + .FF = CAN_frame_std, + }}, + .MsgID = 0x536, + .data = {0x00, 0x40, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Network manage frame +CAN_frame_t VOLVO_372 = { + .FIR = {.B = + { + .DLC = 8, + .FF = CAN_frame_std, + }}, + .MsgID = 0x372, + .data = {0x00, 0xA6, 0x07, 0x14, 0x04, 0x00, 0x80, 0x00}}; //Ambient Temp -->>VERIFY this data content!!!<<-- +CAN_frame_t VOLVO_CELL_U_Req = {.FIR = {.B = + { + .DLC = 8, + .FF = CAN_frame_std, + }}, + .MsgID = 0x735, + .data = {0x03, 0x22, 0x4B, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Cell voltage request frame +CAN_frame_t VOLVO_FlowControl = {.FIR = {.B = + { + .DLC = 8, + .FF = CAN_frame_std, + }}, + .MsgID = 0x735, + .data = {0x30, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00}}; //Flowcontrol +CAN_frame_t VOLVO_SOH_Req = {.FIR = {.B = + { + .DLC = 8, + .FF = CAN_frame_std, + }}, + .MsgID = 0x735, + .data = {0x03, 0x22, 0x49, 0x6D, 0x00, 0x00, 0x00, 0x00}}; //Battery SOH request frame + +void update_values_battery() { //This function maps all the values fetched via CAN to the correct parameters used for the inverter + uint8_t cnt = 0; + + remaining_capacity = (78200 - CHARGE_ENERGY); + + //system_real_SOC_pptt = SOC_BMS; // Use BMS reported SOC, havent figured out how to get the BMS to calibrate empty/full yet + SOC_CALC = remaining_capacity / 78; // Use calculated SOC based on remaining_capacity + + system_real_SOC_pptt = SOC_CALC * 10; + + if (BATT_U > MAX_U) // Protect if overcharged + { + system_real_SOC_pptt = 10000; + } else if (BATT_U < MIN_U) //Protect if undercharged + { + system_real_SOC_pptt = 0; + } + + system_battery_voltage_dV = BATT_U * 10; + system_battery_current_dA = BATT_I * 10; + system_capacity_Wh = BATTERY_WH_MAX; + system_remaining_capacity_Wh = remaining_capacity; // Will wrap! Known limitation due to uint16_t size. + + //system_max_discharge_power_W = HvBattPwrLimDchaSoft * 1000; // Use power limit reported from BMS, not trusted ATM + system_max_discharge_power_W = 30000; + system_max_charge_power_W = 30000; + system_active_power_W = (BATT_U)*BATT_I; + system_temperature_min_dC = BATT_T_MIN; + system_temperature_max_dC = BATT_T_MAX; + + system_cell_max_voltage_mV = CELL_U_MAX * 10; // Use min/max reported from BMS + system_cell_min_voltage_mV = CELL_U_MIN * 10; + + //Map all cell voltages to the global array + for (int i = 0; i < 108; ++i) { + system_cellvoltages_mV[i] = cell_voltages[i]; + } + + /* Check if the BMS is still sending CAN messages. If we go 60s without messages we raise an error*/ + if (!CANstillAlive) { + set_event(EVENT_CAN_RX_FAILURE, 0); + } else { + CANstillAlive--; + clear_event(EVENT_CAN_RX_FAILURE); + } + +#ifdef DEBUG_VIA_USB + Serial.print("BMS reported SOC%: "); + Serial.println(SOC_BMS); + Serial.print("Calculated SOC%: "); + Serial.println(SOC_CALC); + Serial.print("Rescaled SOC%: "); + Serial.println(system_scaled_SOC_pptt / 10); + Serial.print("Battery current: "); + Serial.println(BATT_I); + Serial.print("Battery voltage: "); + Serial.println(BATT_U); + Serial.print("Battery maximum voltage limit: "); + Serial.println(MAX_U); + Serial.print("Battery minimum voltage limit: "); + Serial.println(MIN_U); + Serial.print("Remaining Energy: "); + Serial.println(remaining_capacity); + Serial.print("Discharge limit: "); + Serial.println(HvBattPwrLimDchaSoft); + Serial.print("Battery Error Indication: "); + Serial.println(BATT_ERR_INDICATION); + Serial.print("Maximum battery temperature: "); + Serial.println(BATT_T_MAX / 10); + Serial.print("Minimum battery temperature: "); + Serial.println(BATT_T_MIN / 10); + Serial.print("Average battery temperature: "); + Serial.println(BATT_T_AVG / 10); + Serial.print("BMS Highest cell voltage: "); + Serial.println(CELL_U_MAX * 10); + Serial.print("BMS Lowest cell voltage: "); + Serial.println(CELL_U_MIN * 10); + Serial.print("BMS Highest cell nr: "); + Serial.println(CELL_ID_U_MAX); + Serial.print("Highest cell voltage: "); + Serial.println(min_max_voltage[1]); + Serial.print("Lowest cell voltage: "); + Serial.println(min_max_voltage[0]); + Serial.print("Cell deviation voltage: "); + Serial.println(cell_deviation_mV); + Serial.print("Cell voltage,"); + while (cnt < 108) { + Serial.print(cell_voltages[cnt++]); + Serial.print(","); + } + Serial.println(";"); +#endif +} + +void receive_can_battery(CAN_frame_t rx_frame) { + CANstillAlive = 12; + switch (rx_frame.MsgID) { + case 0x3A: + if ((rx_frame.data.u8[6] & 0x80) == 0x80) + BATT_I = (0 - ((((rx_frame.data.u8[6] & 0x7F) * 256.0 + rx_frame.data.u8[7]) * 0.1) - 1638)); + else { + BATT_I = 0; +#ifdef DEBUG_VIA_USB + Serial.println("BATT_I not valid"); +#endif + } + + if ((rx_frame.data.u8[2] & 0x08) == 0x08) + MAX_U = (((rx_frame.data.u8[2] & 0x07) * 256.0 + rx_frame.data.u8[3]) * 0.25); + else { + //MAX_U = 0; + //Serial.println("MAX_U not valid"); // Value toggles between true/false from BMS + } + + if ((rx_frame.data.u8[4] & 0x08) == 0x08) + MIN_U = (((rx_frame.data.u8[4] & 0x07) * 256.0 + rx_frame.data.u8[5]) * 0.25); + else { + //MIN_U = 0; + //Serial.println("MIN_U not valid"); // Value toggles between true/false from BMS + } + + if ((rx_frame.data.u8[0] & 0x08) == 0x08) + BATT_U = (((rx_frame.data.u8[0] & 0x07) * 256.0 + rx_frame.data.u8[1]) * 0.25); + else { + BATT_U = 0; +#ifdef DEBUG_VIA_USB + Serial.println("BATT_U not valid"); +#endif + } + break; + case 0x1A1: + if ((rx_frame.data.u8[4] & 0x10) == 0x10) + CHARGE_ENERGY = ((((rx_frame.data.u8[4] & 0x0F) * 256.0 + rx_frame.data.u8[5]) * 50) - 500); + else { + CHARGE_ENERGY = 0; + set_event(EVENT_KWH_PLAUSIBILITY_ERROR, CHARGE_ENERGY); + } + break; + case 0x413: + if ((rx_frame.data.u8[0] & 0x80) == 0x80) + BATT_ERR_INDICATION = ((rx_frame.data.u8[0] & 0x40) >> 6); + else { + BATT_ERR_INDICATION = 0; +#ifdef DEBUG_VIA_USB + Serial.println("BATT_ERR_INDICATION not valid"); +#endif + } + if ((rx_frame.data.u8[0] & 0x20) == 0x20) { + BATT_T_MAX = ((rx_frame.data.u8[2] & 0x1F) * 256.0 + rx_frame.data.u8[3]); + BATT_T_MIN = ((rx_frame.data.u8[4] & 0x1F) * 256.0 + rx_frame.data.u8[5]); + BATT_T_AVG = ((rx_frame.data.u8[0] & 0x1F) * 256.0 + rx_frame.data.u8[1]); + } else { + BATT_T_MAX = 0; + BATT_T_MIN = 0; + BATT_T_AVG = 0; +#ifdef DEBUG_VIA_USB + Serial.println("BATT_T not valid"); +#endif + } + break; + case 0x369: + if ((rx_frame.data.u8[0] & 0x80) == 0x80) { + HvBattPwrLimDchaSoft = (((rx_frame.data.u8[6] & 0x03) * 256 + rx_frame.data.u8[6]) >> 2); + } else { + HvBattPwrLimDchaSoft = 0; +#ifdef DEBUG_VIA_USB + Serial.println("HvBattPwrLimDchaSoft not valid"); +#endif + } + break; + case 0x37D: + if ((rx_frame.data.u8[0] & 0x40) == 0x40) { + SOC_BMS = ((rx_frame.data.u8[6] & 0x03) * 256 + rx_frame.data.u8[7]); + } else { + SOC_BMS = 0; +#ifdef DEBUG_VIA_USB + Serial.println("SOC_BMS not valid"); +#endif + } + + if ((rx_frame.data.u8[0] & 0x04) == 0x04) + CELL_U_MAX = ((rx_frame.data.u8[2] & 0x01) * 256 + rx_frame.data.u8[3]); + else { + CELL_U_MAX = 0; +#ifdef DEBUG_VIA_USB + Serial.println("CELL_U_MAX not valid"); +#endif + } + + if ((rx_frame.data.u8[0] & 0x02) == 0x02) + CELL_U_MIN = ((rx_frame.data.u8[0] & 0x01) * 256.0 + rx_frame.data.u8[1]); + else { + CELL_U_MIN = 0; +#ifdef DEBUG_VIA_USB + Serial.println("CELL_U_MIN not valid"); +#endif + } + + if ((rx_frame.data.u8[0] & 0x08) == 0x08) + CELL_ID_U_MAX = ((rx_frame.data.u8[4] & 0x01) * 256.0 + rx_frame.data.u8[5]); + else { + CELL_ID_U_MAX = 0; +#ifdef DEBUG_VIA_USB + Serial.println("CELL_ID_U_MAX not valid"); +#endif + } + break; + case 0x635: // Diag request response + if ((rx_frame.data.u8[0] == 0x07) && (rx_frame.data.u8[1] == 0x62) && (rx_frame.data.u8[2] == 0x49) && + (rx_frame.data.u8[3] == 0x6D)) // SOH response frame + { + system_SOH_pptt = ((rx_frame.data.u8[6] << 8) | rx_frame.data.u8[7]); + } else if ((rx_frame.data.u8[0] == 0x10) && (rx_frame.data.u8[1] == 0x0B) && (rx_frame.data.u8[2] == 0x62) && + (rx_frame.data.u8[3] == 0x4B)) // First response frame of cell voltages + { + cell_voltages[battery_request_idx++] = ((rx_frame.data.u8[5] << 8) | rx_frame.data.u8[6]); + cell_voltages[battery_request_idx] = (rx_frame.data.u8[7] << 8); + ESP32Can.CANWriteFrame(&VOLVO_FlowControl); // Send flow control + rxConsecutiveFrames = 1; + } else if ((rx_frame.data.u8[0] == 0x21) && (rxConsecutiveFrames == 1)) { + cell_voltages[battery_request_idx++] = cell_voltages[battery_request_idx] | rx_frame.data.u8[1]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[2] << 8) | rx_frame.data.u8[3]; + cell_voltages[battery_request_idx++] = (rx_frame.data.u8[4] << 8) | rx_frame.data.u8[5]; + + if (batteryModuleNumber <= 0x2A) // Run until last pack is read + { + VOLVO_CELL_U_Req.data.u8[3] = batteryModuleNumber++; + ESP32Can.CANWriteFrame(&VOLVO_CELL_U_Req); //Send cell voltage read request for next module + } else { + min_max_voltage[0] = 9999; + min_max_voltage[1] = 0; + for (cellcounter = 0; cellcounter < 108; cellcounter++) { + if (min_max_voltage[0] > cell_voltages[cellcounter]) + min_max_voltage[0] = cell_voltages[cellcounter]; + if (min_max_voltage[1] < cell_voltages[cellcounter]) + min_max_voltage[1] = cell_voltages[cellcounter]; + } + + cell_deviation_mV = (min_max_voltage[1] - min_max_voltage[0]); + + if (cell_deviation_mV > MAX_CELL_DEVIATION) { + set_event(EVENT_CELL_DEVIATION_HIGH, 0); +#ifdef DEBUG_VIA_USB + Serial.println("HIGH CELL DEVIATION!!! Inspect battery!"); +#endif + } + + if (min_max_voltage[1] >= MAX_CELL_VOLTAGE) { + system_bms_status = FAULT; + set_event(EVENT_CELL_OVER_VOLTAGE, 0); +#ifdef DEBUG_VIA_USB + Serial.println("CELL OVERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!"); +#endif + } + if (min_max_voltage[0] <= MIN_CELL_VOLTAGE) { + system_bms_status = FAULT; + set_event(EVENT_CELL_UNDER_VOLTAGE, 0); +#ifdef DEBUG_VIA_USB + Serial.println("CELL UNDERVOLTAGE!!! Stopping battery charging and discharging. Inspect battery!"); +#endif + } + ESP32Can.CANWriteFrame(&VOLVO_SOH_Req); //Send SOH read request + } + rxConsecutiveFrames = 0; + } + break; + default: + break; + } +} + +void readCellVoltages() { + battery_request_idx = 0; + batteryModuleNumber = 0x10; + rxConsecutiveFrames = 0; + VOLVO_CELL_U_Req.data.u8[3] = batteryModuleNumber++; + ESP32Can.CANWriteFrame(&VOLVO_CELL_U_Req); //Send cell voltage read request for first module +} + +void send_can_battery() { + unsigned long currentMillis = millis(); + // Send 100ms CAN Message + if (currentMillis - previousMillis100 >= interval100) { + previousMillis100 = currentMillis; + ESP32Can.CANWriteFrame(&VOLVO_536); //Send 0x536 Network managing frame to keep BMS alive + ESP32Can.CANWriteFrame(&VOLVO_372); //Send 0x372 ECMAmbientTempCalculated + + if (system_bms_status == ACTIVE) { + batteryAllowsContactorClosing = true; + } else { //system_bms_status == FAULT or inverter requested opening contactors + batteryAllowsContactorClosing = false; + } + } + if (currentMillis - previousMillis60s >= interval60s) { + previousMillis60s = currentMillis; + if (system_bms_status == ACTIVE) { + readCellVoltages(); + } + } +} + +void setup_battery(void) { // Performs one time setup at startup + Serial.println("Volvo SPA XC40 Recharge / Polestar2 78kWh battery selected"); + + system_number_of_cells = 108; + system_max_design_voltage_dV = 4540; // 454.0V, over this, charging is not possible (goes into forced discharge) + system_min_design_voltage_dV = 2938; // 293.8V under this, discharging further is disabled +} +#endif diff --git a/Software/src/battery/VOLVO-SPA-BATTERY.h b/Software/src/battery/VOLVO-SPA-BATTERY.h new file mode 100644 index 00000000..72691dfe --- /dev/null +++ b/Software/src/battery/VOLVO-SPA-BATTERY.h @@ -0,0 +1,34 @@ +#ifndef VOLVO_SPA_BATTERY_H +#define VOLVO_SPA_BATTERY_H +#include +#include "../../USER_SETTINGS.h" +#include "../devboard/config.h" // Needed for all defines +#include "../lib/miwagner-ESP32-Arduino-CAN/ESP32CAN.h" + +#define BATTERY_SELECTED + +// These parameters need to be mapped for the inverter +extern uint32_t system_capacity_Wh; //Wh, 0-150000Wh +extern uint32_t system_remaining_capacity_Wh; //Wh, 0-150000Wh +extern int16_t system_temperature_min_dC; //C+1, -50.0 - 50.0 +extern int16_t system_temperature_max_dC; //C+1, -50.0 - 50.0 +extern int16_t system_active_power_W; //W, -32000 to 32000 +extern int16_t system_battery_current_dA; //A+1, -1000 - 1000 +extern uint16_t system_battery_voltage_dV; //V+1, 0-500.0 (0-5000) +extern uint16_t system_max_design_voltage_dV; //V+1, 0-500.0 (0-5000) +extern uint16_t system_min_design_voltage_dV; //V+1, 0-500.0 (0-5000) +extern uint16_t system_scaled_SOC_pptt; //SOC%, 0-100.00 (0-10000) +extern uint16_t system_real_SOC_pptt; //SOC%, 0-100.00 (0-10000) +extern uint16_t system_SOH_pptt; //SOH%, 0-100.00 (0-10000) +extern uint16_t system_max_discharge_power_W; //W, 0-65000 +extern uint16_t system_max_charge_power_W; //W, 0-65000 +extern uint16_t system_cell_max_voltage_mV; //mV, 0-5000, Stores the highest cell millivolt value +extern uint16_t system_cell_min_voltage_mV; //mV, 0-5000, Stores the minimum cell millivolt value +extern uint16_t system_cellvoltages_mV[MAX_AMOUNT_CELLS]; //Array with all cell voltages in mV +extern uint8_t system_number_of_cells; //Total number of cell voltages, set by each battery +extern uint8_t system_bms_status; //Enum 0-5 +extern bool batteryAllowsContactorClosing; //Bool, true/false + +void setup_battery(void); + +#endif diff --git a/Software/src/devboard/webserver/webserver.cpp b/Software/src/devboard/webserver/webserver.cpp index 92f83edc..aa1e7914 100644 --- a/Software/src/devboard/webserver/webserver.cpp +++ b/Software/src/devboard/webserver/webserver.cpp @@ -450,6 +450,9 @@ String processor(const String& var) { #ifdef TESLA_MODEL_3_BATTERY content += "Tesla Model S/3/X/Y"; #endif +#ifdef VOLVO_SPA_BATTERY + content += "Volvo / Polestar 78kWh battery"; +#endif #ifdef TEST_FAKE_BATTERY content += "Fake battery for testing purposes"; #endif