diff --git a/Software/Software.ino b/Software/Software.ino index 30afdb1c8..015aed708 100644 --- a/Software/Software.ino +++ b/Software/Software.ino @@ -124,6 +124,9 @@ void setup() { setup_battery(); #ifdef EQUIPMENT_STOP_BUTTON init_equipment_stop_button(); +#endif +#ifdef CAN_SHUNT_SELECTED + setup_can_shunt(); #endif // BOOT button at runtime is used as an input for various things pinMode(0, INPUT_PULLUP); diff --git a/Software/USER_SETTINGS.cpp b/Software/USER_SETTINGS.cpp index 0ed5c8477..dc3363e27 100644 --- a/Software/USER_SETTINGS.cpp +++ b/Software/USER_SETTINGS.cpp @@ -17,7 +17,8 @@ volatile CAN_Configuration can_config = { .battery = CAN_NATIVE, // Which CAN is your battery connected to? .inverter = CAN_NATIVE, // Which CAN is your inverter connected to? (No need to configure incase you use RS485) .battery_double = CAN_ADDON_MCP2515, // (OPTIONAL) Which CAN is your second battery connected to? - .charger = CAN_NATIVE // (OPTIONAL) Which CAN is your charger connected to? + .charger = CAN_NATIVE, // (OPTIONAL) Which CAN is your charger connected to? + .shunt = CAN_NATIVE // (OPTIONAL) Which CAN is your shunt connected to? }; std::string ssid = WIFI_SSID; // Set in USER_SECRETS.h diff --git a/Software/USER_SETTINGS.h b/Software/USER_SETTINGS.h index 2135db579..0d60bdd00 100644 --- a/Software/USER_SETTINGS.h +++ b/Software/USER_SETTINGS.h @@ -67,6 +67,9 @@ //#define PWM_CONTACTOR_CONTROL //Enable this line to use PWM for CONTACTOR_CONTROL, which lowers power consumption and heat generation. CONTACTOR_CONTROL must be enabled. //#define NC_CONTACTORS //Enable this line to control normally closed contactors. CONTACTOR_CONTROL must be enabled for this option. Extremely rare setting! +/* Shunt/Contactor settings */ +//#define BMW_SBOX // SBOX relay control & battery current/voltage measurement + /* Other options */ //#define DEBUG_VIA_USB //Enable this line to have the USB port output serial diagnostic data while program runs (WARNING, raises CPU load, do not use for production) //#define DEBUG_VIA_WEB //Enable this line to log diagnostic data while program runs, which can be viewed via webpage (WARNING, slightly raises CPU load, do not use for production) @@ -143,6 +146,7 @@ typedef struct { CAN_Interface inverter; CAN_Interface battery_double; CAN_Interface charger; + CAN_Interface shunt; } CAN_Configuration; extern volatile CAN_Configuration can_config; extern volatile uint8_t AccessPointEnabled; diff --git a/Software/src/battery/BATTERIES.h b/Software/src/battery/BATTERIES.h index a4778920a..93a96f9b0 100644 --- a/Software/src/battery/BATTERIES.h +++ b/Software/src/battery/BATTERIES.h @@ -2,6 +2,13 @@ #define BATTERIES_H #include "../../USER_SETTINGS.h" +#ifdef BMW_SBOX +#include "BMW-SBOX.h" +void handle_incoming_can_frame_shunt(CAN_frame rx_frame); +void transmit_can_shunt(); +void setup_can_shunt(); +#endif + #ifdef BMW_I3_BATTERY #include "BMW-I3-BATTERY.h" #endif diff --git a/Software/src/battery/BMW-SBOX.cpp b/Software/src/battery/BMW-SBOX.cpp new file mode 100644 index 000000000..ff0139b45 --- /dev/null +++ b/Software/src/battery/BMW-SBOX.cpp @@ -0,0 +1,218 @@ +#include "../include.h" +#ifdef BMW_SBOX +#include "../datalayer/datalayer.h" +#include "BMW-SBOX.h" + +#define MAX_ALLOWED_FAULT_TICKS 1000 + +enum SboxState { DISCONNECTED, PRECHARGE, NEGATIVE, POSITIVE, PRECHARGE_OFF, COMPLETED, SHUTDOWN_REQUESTED }; +SboxState contactorStatus = DISCONNECTED; + +unsigned long prechargeStartTime = 0; +unsigned long negativeStartTime = 0; +unsigned long positiveStartTime = 0; +unsigned long timeSpentInFaultedMode = 0; +unsigned long LastMsgTime = 0; // will store last time a 20ms CAN Message was send +unsigned long LastAvgTime = 0; // Last current storage time +unsigned long ShuntLastSeen = 0; + +uint32_t avg_mA_array[10]; +uint32_t avg_sum; + +uint8_t k; //avg array pointer + +uint8_t CAN100_cnt = 0; + +CAN_frame SBOX_100 = {.FD = false, + .ext_ID = false, + .DLC = 4, + .ID = 0x100, + .data = {0x55, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, + 0x00}}; // Byte 0: relay control, Byte 1: counter 0-E, Byte 4: CRC + +CAN_frame SBOX_300 = {.FD = false, + .ext_ID = false, + .DLC = 4, + .ID = 0x300, + .data = {0xFF, 0xFE, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00}}; // Static frame + +uint8_t reverse_bits(uint8_t byte) { + uint8_t reversed = 0; + for (int i = 0; i < 8; i++) { + reversed = (reversed << 1) | (byte & 1); + byte >>= 1; + } + return reversed; +} + +/** CRC8, both inverted, poly 0x31 **/ +uint8_t calculateCRC(CAN_frame CAN) { + uint8_t crc = 0; + for (size_t i = 0; i < CAN.DLC; i++) { + uint8_t reversed_byte = reverse_bits(CAN.data.u8[i]); + crc ^= reversed_byte; + for (int j = 0; j < 8; j++) { + if (crc & 0x80) { + crc = (crc << 1) ^ 0x31; + } else { + crc <<= 1; + } + crc &= 0xFF; + } + } + crc = reverse_bits(crc); + return crc; +} + +void handle_incoming_can_frame_shunt(CAN_frame rx_frame) { + unsigned long currentTime = millis(); + if (rx_frame.ID == 0x200) { + ShuntLastSeen = currentTime; + datalayer.shunt.measured_amperage_mA = + ((rx_frame.data.u8[2] << 24) | (rx_frame.data.u8[1] << 16) | (rx_frame.data.u8[0] << 8)) / 256; + datalayer.shunt.measured_amperage_dA = datalayer.shunt.measured_amperage_mA / 100; + + /** Calculate 1S avg current **/ + if (LastAvgTime + 100 < currentTime) { + LastAvgTime = currentTime; + if (k > 9) { + k = 0; + } + avg_mA_array[k] = datalayer.shunt.measured_amperage_mA; + k++; + avg_sum = 0; + for (uint8_t i = 0; i < 10; i++) { + avg_sum = avg_sum + avg_mA_array[i]; + } + datalayer.shunt.measured_avg1S_amperage_mA = avg_sum / 10; + } + } else if (rx_frame.ID == 0x210) //SBOX input (battery side) voltage + { + ShuntLastSeen = currentTime; + datalayer.shunt.measured_voltage_mV = + ((rx_frame.data.u8[2] << 16) | (rx_frame.data.u8[1] << 8) | (rx_frame.data.u8[0])); + } else if (rx_frame.ID == 0x220) //SBOX output voltage + { + ShuntLastSeen = currentTime; + datalayer.shunt.measured_outvoltage_mV = + ((rx_frame.data.u8[2] << 16) | (rx_frame.data.u8[1] << 8) | (rx_frame.data.u8[0])); + datalayer.shunt.available = true; + } +} + +void transmit_can_shunt() { + unsigned long currentTime = millis(); + + /** Shunt can frames seen? **/ + if (ShuntLastSeen + 1000 < currentTime) { + datalayer.shunt.available = false; + } else { + datalayer.shunt.available = true; + } + // Send 20ms CAN Message + if (currentTime - LastMsgTime >= INTERVAL_20_MS) { + LastMsgTime = currentTime; + // First check if we have any active errors, incase we do, turn off the battery + if (datalayer.battery.status.bms_status == FAULT) { + timeSpentInFaultedMode++; + } else { + timeSpentInFaultedMode = 0; + } + + //handle contactor control SHUTDOWN_REQUESTED + if (timeSpentInFaultedMode > MAX_ALLOWED_FAULT_TICKS) { + contactorStatus = SHUTDOWN_REQUESTED; + SBOX_100.data.u8[0] = 0x55; // All open + } + + if (contactorStatus == SHUTDOWN_REQUESTED) { + datalayer.shunt.contactors_engaged = false; + return; // A fault scenario latches the contactor control. It is not possible to recover without a powercycle (and investigation why fault occured) + } + + // After that, check if we are OK to start turning on the contactors + if (contactorStatus == DISCONNECTED) { + datalayer.shunt.contactors_engaged = false; + SBOX_100.data.u8[0] = 0x55; // All open + + if (datalayer.system.status.battery_allows_contactor_closing && + datalayer.system.status.inverter_allows_contactor_closing && + !datalayer.system.settings.equipment_stop_active && + (datalayer.shunt.measured_voltage_mV > MINIMUM_INPUT_VOLTAGE * 1000)) { + contactorStatus = PRECHARGE; + } + } + // In case the inverter requests contactors to open, set the state accordingly + if (contactorStatus == COMPLETED) { + //Incase inverter (or estop) requests contactors to open, make state machine jump to Disconnected state (recoverable) + if (!datalayer.system.status.inverter_allows_contactor_closing || + datalayer.system.settings.equipment_stop_active) { + contactorStatus = DISCONNECTED; + } + } + // Handle actual state machine. This first turns on Precharge, then Negative, then Positive, and finally turns OFF precharge + switch (contactorStatus) { + case PRECHARGE: + SBOX_100.data.u8[0] = 0x86; // Precharge relay only + prechargeStartTime = currentTime; + contactorStatus = NEGATIVE; +#ifdef DEBUG_VIA_USB + Serial.println("S-BOX Precharge relay engaged"); +#endif + break; + case NEGATIVE: + if (currentTime - prechargeStartTime >= CONTACTOR_CONTROL_T1) { + SBOX_100.data.u8[0] = 0xA6; // Precharge + Negative + negativeStartTime = currentTime; + contactorStatus = POSITIVE; + datalayer.shunt.precharging = true; +#ifdef DEBUG_VIA_USB + Serial.println("S-BOX Negative relay engaged"); +#endif + } + break; + case POSITIVE: + if (currentTime - negativeStartTime >= CONTACTOR_CONTROL_T2 && + (datalayer.shunt.measured_voltage_mV * MAX_PRECHARGE_RESISTOR_VOLTAGE_PERCENT < + datalayer.shunt.measured_outvoltage_mV)) { + SBOX_100.data.u8[0] = 0xAA; // Precharge + Negative + Positive + positiveStartTime = currentTime; + contactorStatus = PRECHARGE_OFF; + datalayer.shunt.precharging = false; +#ifdef DEBUG_VIA_USB + Serial.println("S-BOX Positive relay engaged"); +#endif + } + break; + case PRECHARGE_OFF: + if (currentTime - positiveStartTime >= CONTACTOR_CONTROL_T3) { + SBOX_100.data.u8[0] = 0x6A; // Negative + Positive + contactorStatus = COMPLETED; +#ifdef DEBUG_VIA_USB + Serial.println("S-BOX Precharge relay released"); +#endif + datalayer.shunt.contactors_engaged = true; + } + break; + case COMPLETED: + SBOX_100.data.u8[0] = 0x6A; // Negative + Positive + default: + break; + } + CAN100_cnt++; + if (CAN100_cnt > 0x0E) { + CAN100_cnt = 0; + } + SBOX_100.data.u8[1] = CAN100_cnt << 4 | 0x01; + SBOX_100.data.u8[3] = 0x00; + SBOX_100.data.u8[3] = calculateCRC(SBOX_100); + transmit_can_frame(&SBOX_100, can_config.shunt); + transmit_can_frame(&SBOX_300, can_config.shunt); + } +} + +void setup_can_shunt() { + strncpy(datalayer.system.info.shunt_protocol, "BMW SBOX", 63); + datalayer.system.info.shunt_protocol[63] = '\0'; +} +#endif diff --git a/Software/src/battery/BMW-SBOX.h b/Software/src/battery/BMW-SBOX.h new file mode 100644 index 000000000..131fa3a0e --- /dev/null +++ b/Software/src/battery/BMW-SBOX.h @@ -0,0 +1,22 @@ +#ifndef BMW_SBOX_CONTROL_H +#define BMW_SBOX_CONTROL_H +#include "../include.h" +#define CAN_SHUNT_SELECTED +void transmit_can(CAN_frame* tx_frame, int interface); + +/** Minimum input voltage required to enable relay control **/ +#define MINIMUM_INPUT_VOLTAGE 250 + +/** Minimum required percentage of input voltage at the output port to engage the positive relay. **/ +/** Prevents engagement of the positive contactor if the precharge resistor is faulty. **/ +#define MAX_PRECHARGE_RESISTOR_VOLTAGE_PERCENT 0.99 + +/* NOTE: modify the T2 time constant below to account for the resistance and capacitance of the target system. + * t=3RC at minimum, t=5RC ideally + */ + +#define CONTACTOR_CONTROL_T1 5000 // Time before negative contactor engages and precharging starts +#define CONTACTOR_CONTROL_T2 5000 // Precharge time before precharge resistor is bypassed by positive contactor +#define CONTACTOR_CONTROL_T3 2000 // Precharge relay lead time after positive contactor has been engaged + +#endif diff --git a/Software/src/communication/can/comm_can.cpp b/Software/src/communication/can/comm_can.cpp index ae9fb0976..e85eca7e6 100644 --- a/Software/src/communication/can/comm_can.cpp +++ b/Software/src/communication/can/comm_can.cpp @@ -116,6 +116,10 @@ void transmit_can() { #ifdef CHARGER_SELECTED transmit_can_charger(); #endif // CHARGER_SELECTED + +#ifdef CAN_SHUNT_SELECTED + transmit_can_shunt(); +#endif // CAN_SHUNT_SELECTED } void transmit_can_frame(CAN_frame* tx_frame, int interface) { @@ -339,6 +343,11 @@ void map_can_frame_to_variable(CAN_frame* rx_frame, int interface) { if (interface == can_config.charger) { #ifdef CHARGER_SELECTED map_can_frame_to_variable_charger(*rx_frame); +#endif + } + if (interface == can_config.shunt) { +#ifdef CAN_SHUNT_SELECTED + handle_incoming_can_frame_shunt(*rx_frame); #endif } } diff --git a/Software/src/datalayer/datalayer.h b/Software/src/datalayer/datalayer.h index 6064aa153..7fce01227 100644 --- a/Software/src/datalayer/datalayer.h +++ b/Software/src/datalayer/datalayer.h @@ -134,6 +134,20 @@ typedef struct { uint16_t measured_voltage_dV = 0; /** measured amperage in deciAmperes. 300 = 30.0 A */ uint16_t measured_amperage_dA = 0; + /** measured battery voltage in mV (S-BOX) **/ + uint32_t measured_voltage_mV = 0; + /** measured output voltage in mV (eg. S-BOX) **/ + uint32_t measured_outvoltage_mV = 0; + /** measured amperage in mA (eg. S-BOX) **/ + int32_t measured_amperage_mA = 0; + /** Average current from last 1s **/ + int32_t measured_avg1S_amperage_mA = 0; + /** True if contactors are precharging state */ + bool precharging = false; + /** True if the contactor controlled by battery-emulator is closed */ + bool contactors_engaged = false; + /** True if shunt communication ok **/ + bool available = false; } DATALAYER_SHUNT_TYPE; typedef struct { @@ -141,6 +155,8 @@ typedef struct { char battery_protocol[64] = {0}; /** array with type of inverter used, for displaying on webserver */ char inverter_protocol[64] = {0}; + /** array with type of battery used, for displaying on webserver */ + char shunt_protocol[64] = {0}; /** array with incoming CAN messages, for displaying on webserver */ char logged_can_messages[15000] = {0}; size_t logged_can_messages_offset = 0; diff --git a/Software/src/devboard/webserver/settings_html.cpp b/Software/src/devboard/webserver/settings_html.cpp index e21ec7229..fd5be7b77 100644 --- a/Software/src/devboard/webserver/settings_html.cpp +++ b/Software/src/devboard/webserver/settings_html.cpp @@ -41,6 +41,11 @@ String settings_processor(const String& var) { content += "