diff --git a/Software/SOLAX-CAN.cpp b/Software/SOLAX-CAN.cpp index 26ade1b1..bff49672 100644 --- a/Software/SOLAX-CAN.cpp +++ b/Software/SOLAX-CAN.cpp @@ -1,35 +1,61 @@ #include "SOLAX-CAN.h" -#include "ESP32CAN.h" -#include "CAN_config.h" /* Do not change code below unless you are sure what you are doing */ -static unsigned long previousMillis100ms = 0; // will store last time a 100ms CAN Message was sent -static const int interval100ms = 100; // interval (ms) at which send CAN Messages -static int temp = 0; //Temporary variable used for bitshifting static int max_charge_rate_amp = 0; static int max_discharge_rate_amp = 0; +static int temperature_average = 0; +static int STATE = BATTERY_ANNOUNCE; +static unsigned long LastFrameTime = 0; +static unsigned short BatteryModuleFirmware = 2; //CAN message translations from this amazing repository: https://github.com/rand12345/solax_can_bus -CAN_frame_t SOLAX_1872 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1872,.data = {0x8A, 0xF, 0x52, 0xC, 0xCD, 0x0, 0x5E, 0x1}}; //BMS_Limits -CAN_frame_t SOLAX_1873 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1873,.data = {0x6D, 0xD, 0x0, 0x0, 0x5D, 0x0, 0xA3, 0x1}}; //BMS_PackData -CAN_frame_t SOLAX_1874 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1874,.data = {0xCE, 0x0, 0xBC, 0x0, 0x29, 0x0, 0x28, 0x0}}; //BMS_CellData -CAN_frame_t SOLAX_1875 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1875,.data = {0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x3A, 0x0}}; //BMS_Status -CAN_frame_t SOLAX_1876 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1876,.data = {0x0, 0x0, 0xD4, 0x0F, 0x0, 0x0, 0xC9, 0x0F}}; //BMS_PackTemps -CAN_frame_t SOLAX_1877 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1877,.data = {0x0, 0x0, 0x0, 0x0, 0x53, 0x0, 0x1D, 0x10}}; -CAN_frame_t SOLAX_1878 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1878,.data = {0x6D, 0xD, 0x0, 0x0, 0xB0, 0x3, 0x4, 0x0}}; //BMS_PackStats -CAN_frame_t SOLAX_1879 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1879,.data = {0x1, 0x8, 0x1, 0x2, 0x1, 0x2, 0x0, 0x3}}; -CAN_frame_t SOLAX_1801 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1801,.data = {0x2, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0}}; -CAN_frame_t SOLAX_1881 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1881,.data = {0x0, 0x36, 0x53, 0x42, 0x4D, 0x53, 0x46, 0x41}}; // 0 6 S B M S F A -CAN_frame_t SOLAX_1882 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1882,.data = {0x0, 0x32, 0x33, 0x41, 0x42, 0x30, 0x35, 0x32}}; // 0 2 3 A B 0 5 2 -CAN_frame_t SOLAX_100A001 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x100A001,.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}; +CAN_frame_t SOLAX_1801 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1801,.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}; +CAN_frame_t SOLAX_1872 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1872,.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}; //BMS_Limits +CAN_frame_t SOLAX_1873 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1873,.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}; //BMS_PackData +CAN_frame_t SOLAX_1874 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1874,.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}; //BMS_CellData +CAN_frame_t SOLAX_1875 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1875,.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}; //BMS_Status +CAN_frame_t SOLAX_1876 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1876,.data = {0x0, 0x0, 0xE2, 0x0C, 0x0, 0x0, 0xD7, 0x0C}}; //BMS_PackTemps +CAN_frame_t SOLAX_1877 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1877,.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}; +CAN_frame_t SOLAX_1878 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1878,.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}; //BMS_PackStats +CAN_frame_t SOLAX_1879 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1879,.data = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}}; +CAN_frame_t SOLAX_1881 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1881,.data = {0x00, 0x36, 0x53, 0x42, 0x4D, 0x53, 0x46, 0x41}}; // E.g.: 0 6 S B M S F A +CAN_frame_t SOLAX_1882 = {.FIR = {.B = {.DLC = 8,.FF = CAN_frame_ext,}},.MsgID = 0x1882,.data = {0x00, 0x32, 0x33, 0x41, 0x42, 0x30, 0x35, 0x32}}; // E.g.: 0 2 3 A B 0 5 2 +CAN_frame_t SOLAX_100A001 = {.FIR = {.B = {.DLC = 0,.FF = CAN_frame_ext,}},.MsgID = 0x100A001,.data = {}}; + +// __builtin_bswap64 needed to convert to ESP32 little endian format +// Byte[4] defines the requested con-tactor state: 1 = Closed , 0 = Open +#define Contactor_Open_Payload __builtin_bswap64(0x0200010000000000) +#define Contactor_Close_Payload __builtin_bswap64(0x0200010001000000) + +void CAN_WriteFrame(CAN_frame_t* tx_frame) +{ +#ifdef DUAL_CAN + CANMessage MCP2515Frame; //Struct with ACAN2515 library format, needed to use the MCP2515 library + MCP2515Frame.id = tx_frame->MsgID; + MCP2515Frame.ext = tx_frame->FIR.B.FF; + MCP2515Frame.len = tx_frame->FIR.B.DLC; + for (uint8_t i=0 ; idata.u8[i]; + } + can.tryToSend(MCP2515Frame); + //Serial.println("Solax CAN Frame sent in Bus 2"); +#else + ESP32Can.CANWriteFrame(tx_frame); + //Serial.println("Solax CAN Frame sent in Bus 1"); +#endif +} void update_values_can_solax() { //This function maps all the values fetched from battery CAN to the correct CAN messages - - //SOC (100.00%) - temp = SOC/100; //Remove decimals, inverter takes only integer in a byte - SOLAX_1873.data.u8[4] = temp; + // If not receiveing any communication from the inverter, open contactos and return to announce stage + if (millis() - LastFrameTime >= SolaxTimeout) + { + inverterAllowsContactorClosing = 0; + STATE = BATTERY_ANNOUNCE; + } + //Calculate the required values + temperature_average = ((temperature_max + temperature_min)/2); //max_target_charge_power (30000W max) if(SOC > 9999) //99.99% @@ -47,12 +73,6 @@ void update_values_can_solax() max_charge_rate_amp = (max_target_charge_power/(battery_voltage*0.1)); // P/U = I } } - //Increase decimal amount - max_charge_rate_amp = max_charge_rate_amp*10; - - //Write the calculated charge rate to the CAN message - SOLAX_1872.data.u8[4] = (uint8_t) max_charge_rate_amp; //TODO, test that values are OK - SOLAX_1872.data.u8[5] = (max_charge_rate_amp << 8); //max_target_discharge_power (30000W max) if(SOC < 100) //1.00% @@ -70,47 +90,129 @@ void update_values_can_solax() max_discharge_rate_amp = (max_target_discharge_power/(battery_voltage*0.1)); // P/U = I } } - //Increase decimal amount - max_discharge_rate_amp = max_discharge_rate_amp*10; + + //Put the values into the CAN messages - //Write the calculated charge rate to the CAN message - SOLAX_1872.data.u8[6] = (uint8_t) max_discharge_rate_amp; //TODO, test that values are OK - SOLAX_1872.data.u8[7] = (max_discharge_rate_amp << 8); + //BMS_Limits + SOLAX_1872.data.u8[0] = (uint8_t) max_volt_solax_can; //Todo, scaling OK? + SOLAX_1872.data.u8[1] = (max_volt_solax_can >> 8); + SOLAX_1872.data.u8[2] = (uint8_t) min_volt_solax_can; //Todo, scaling OK? + SOLAX_1872.data.u8[3] = (min_volt_solax_can >> 8); + SOLAX_1872.data.u8[4] = (uint8_t) (max_charge_rate_amp*10); //Todo, scaling OK? + SOLAX_1872.data.u8[5] = ((max_charge_rate_amp*10) >> 8); + SOLAX_1872.data.u8[6] = (uint8_t) (max_discharge_rate_amp*10); //Todo, scaling OK? + SOLAX_1872.data.u8[7] = ((max_discharge_rate_amp*10) >> 8); - //Todo (ranked in priority) - //Add current - //Add voltage - //Add remaining kWh - //Add temperature - //Add cell voltages - //Add pack voltage min/max for alarms - //Add cell voltage min/max for alarms + //BMS_PackData + SOLAX_1873.data.u8[0] = (uint8_t) battery_voltage; //Todo, scaling OK? + SOLAX_1873.data.u8[1] = (battery_voltage >> 8); + SOLAX_1873.data.u8[2] = (int8_t) stat_batt_power; //Todo, scaling OK? Signed? + SOLAX_1873.data.u8[3] = (stat_batt_power >> 8); + SOLAX_1873.data.u8[4] = (uint8_t) (SOC/100); //SOC (100.00%) + //SOLAX_1873.data.u8[5] = //Seems like this is not required? Or shall we put SOC decimals here? + SOLAX_1873.data.u8[6] = (uint8_t) (remaining_capacity_Wh/100); //Todo, scaling OK? + SOLAX_1873.data.u8[7] = ((remaining_capacity_Wh/100) >> 8); + + //BMS_CellData + SOLAX_1874.data.u8[0] = (uint8_t) temperature_max; + SOLAX_1874.data.u8[1] = (temperature_max >> 8); + SOLAX_1874.data.u8[2] = (uint8_t) temperature_min; + SOLAX_1874.data.u8[3] = (temperature_min >> 8); + SOLAX_1874.data.u8[4] = (uint8_t) (cell_max_voltage); //Todo, scaling OK? Supposed to be alarm trigger absolute cell max? + SOLAX_1874.data.u8[5] = (cell_max_voltage >> 8); + SOLAX_1874.data.u8[6] = (uint8_t) (cell_min_voltage); //Todo, scaling OK? Supposed to be alarm trigger absolute cell min? + SOLAX_1874.data.u8[7] = (cell_min_voltage >> 8); + + //BMS_Status + SOLAX_1875.data.u8[0] = (uint8_t) temperature_average; + SOLAX_1875.data.u8[1] = (temperature_average >> 8); + SOLAX_1875.data.u8[2] = (uint8_t) 0; // Number of slave batteries + SOLAX_1875.data.u8[4] = (uint8_t) 0; // Contactor Status 0=off, 1=on. + + //BMS_PackTemps (strange name, since it has voltages?) + SOLAX_1876.data.u8[2] = (uint8_t) cell_max_voltage; //Todo, scaling OK? + SOLAX_1876.data.u8[3] = (cell_min_voltage >> 8); + + SOLAX_1876.data.u8[6] = (uint8_t) cell_min_voltage; //Todo, scaling OK? + SOLAX_1876.data.u8[7] = (cell_min_voltage >> 8); + + //Unknown + SOLAX_1877.data.u8[4] = (uint8_t) 0x53; + SOLAX_1877.data.u8[6] = (BatteryModuleFirmware >> 8); + SOLAX_1877.data.u8[7] = (uint8_t) BatteryModuleFirmware; + + //BMS_PackStats + SOLAX_1878.data.u8[0] = (uint8_t) (battery_voltage/10); //TODO, should this be max or current voltage? + SOLAX_1878.data.u8[1] = ((battery_voltage/10) >> 8); + + SOLAX_1878.data.u8[4] = (uint8_t) capacity_Wh; //TODO, scaling OK? + SOLAX_1878.data.u8[5] = (capacity_Wh >> 8); + + // BMS_Answer + SOLAX_1801.data.u8[0] = 2; + SOLAX_1801.data.u8[2] = 1; + SOLAX_1801.data.u8[4] = 1; } -void send_can_solax() -{ - unsigned long currentMillis = millis(); - // Send 100ms CAN Message - if (currentMillis - previousMillis100ms >= interval100ms) - { - previousMillis100ms = currentMillis; - - ESP32Can.CANWriteFrame(&SOLAX_1872); - ESP32Can.CANWriteFrame(&SOLAX_1873); - ESP32Can.CANWriteFrame(&SOLAX_1874); - ESP32Can.CANWriteFrame(&SOLAX_1875); - ESP32Can.CANWriteFrame(&SOLAX_1876); - ESP32Can.CANWriteFrame(&SOLAX_1877); - ESP32Can.CANWriteFrame(&SOLAX_1878); - - //Todo, how often should the messages be sent? And the other messages, only on bootup? - } +void send_can_solax() { + // Deprecated - All transmissions should be initiated in response to inverter polling. } + void receive_can_solax(CAN_frame_t rx_frame) { - //Serial.println("Inverter sending CAN message"); - //0x1871 [0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00] - //Todo, should we respond with something once the inverter sends a message? -} \ No newline at end of file + if (rx_frame.MsgID == 0x1871) { + LastFrameTime = millis(); + switch(STATE) + { + case(BATTERY_ANNOUNCE): + Serial.println("Solax Battery State: Announce"); + inverterAllowsContactorClosing = 0; + SOLAX_1875.data.u8[4] = (0x00); // Inform Inverter: Contactor 0=off, 1=on. + CAN_WriteFrame(&SOLAX_100A001); //BMS Announce + CAN_WriteFrame(&SOLAX_1872); + CAN_WriteFrame(&SOLAX_1873); + CAN_WriteFrame(&SOLAX_1874); + CAN_WriteFrame(&SOLAX_1875); + CAN_WriteFrame(&SOLAX_1876); + CAN_WriteFrame(&SOLAX_1877); + CAN_WriteFrame(&SOLAX_1878); + // Message from the inverter to proceed to contactor closing + // Byte 4 changes from 0 to 1 + if (rx_frame.data.u64 == Contactor_Close_Payload) + STATE = WAITING_FOR_CONTACTOR; + break; + + case(WAITING_FOR_CONTACTOR): + SOLAX_1875.data.u8[4] = (0x00); // Inform Inverter: Contactor 0=off, 1=on. + CAN_WriteFrame(&SOLAX_1872); + CAN_WriteFrame(&SOLAX_1873); + CAN_WriteFrame(&SOLAX_1874); + CAN_WriteFrame(&SOLAX_1875); + CAN_WriteFrame(&SOLAX_1876); + CAN_WriteFrame(&SOLAX_1877); + CAN_WriteFrame(&SOLAX_1878); + CAN_WriteFrame(&SOLAX_1801); // Announce that the battery will be connected + STATE = CONTACTOR_CLOSED; // Jump to Contactor Closed State + Serial.println("Solax Battery State: Contactor Closed"); + break; + + case(CONTACTOR_CLOSED): + inverterAllowsContactorClosing = 1; + SOLAX_1875.data.u8[4] = (0x01); // Inform Inverter: Contactor 0=off, 1=on. + CAN_WriteFrame(&SOLAX_1872); + CAN_WriteFrame(&SOLAX_1873); + CAN_WriteFrame(&SOLAX_1874); + CAN_WriteFrame(&SOLAX_1875); + CAN_WriteFrame(&SOLAX_1876); + CAN_WriteFrame(&SOLAX_1877); + CAN_WriteFrame(&SOLAX_1878); + // Message from the inverter to open contactor + // Byte 4 changes from 1 to 0 + if (rx_frame.data.u64 == Contactor_Open_Payload) + STATE = BATTERY_ANNOUNCE; + break; + } + } +} diff --git a/Software/SOLAX-CAN.h b/Software/SOLAX-CAN.h index 6df213c1..13d7b0ff 100644 --- a/Software/SOLAX-CAN.h +++ b/Software/SOLAX-CAN.h @@ -2,6 +2,11 @@ #define SOLAX_CAN_H #include #include "ESP32CAN.h" +#include "config.h" +#ifdef DUAL_CAN + #include "ACAN2515.h" + extern ACAN2515 can; +#endif extern uint16_t SOC; extern uint16_t StateOfHealth; @@ -17,15 +22,21 @@ extern uint16_t stat_batt_power; extern uint16_t temperature_min; extern uint16_t temperature_max; extern uint16_t CANerror; -extern uint16_t min_volt_byd_can; -extern uint16_t max_volt_byd_can; -// Definitions for BMS status -#define STANDBY 0 -#define INACTIVE 1 -#define DARKSTART 2 -#define ACTIVE 3 -#define FAULT 4 -#define UPDATING 5 +extern uint16_t min_volt_solax_can; +extern uint16_t max_volt_solax_can; +extern uint16_t cell_max_voltage; +extern uint16_t cell_min_voltage; +extern uint8_t inverterAllowsContactorClosing; + +// Timeout in milliseconds +#define SolaxTimeout 2000 + +//SOLAX BMS States Definition +#define BATTERY_ANNOUNCE 0 +#define WAITING_FOR_CONTACTOR 1 +#define CONTACTOR_CLOSED 2 +#define FAULT 3 +#define UPDATING_FW 4 void update_values_can_solax(); void send_can_solax(); diff --git a/Software/Software.ino b/Software/Software.ino index 4fc580d5..3e95de65 100644 --- a/Software/Software.ino +++ b/Software/Software.ino @@ -1,21 +1,5 @@ -/* Select battery used */ -#define BATTERY_TYPE_LEAF // See NISSAN-LEAF-BATTERY.h for more LEAF battery settings -//#define TESLA_MODEL_3_BATTERY // See TESLA-MODEL-3-BATTERY.h for more Tesla battery settings -//#define RENAULT_ZOE_BATTERY // See RENAULT-ZOE-BATTERY.h for more Zoe battery settings -//#define BMW_I3_BATTERY // See BMW-I3-BATTERY.h for more i3 battery settings -//#define IMIEV_ION_CZERO_BATTERY // See IMIEV-CZERO-ION-BATTERY.h for more triplet battery settings -//#define KIA_HYUNDAI_64_BATTERY // See KIA-HYUNDAI-64-BATTERY.h for more battery settings -//#define CHADEMO // See CHADEMO.h for more Chademo related settings - -/* Select inverter communication protocol. See Wiki for which to use with your inverter: https://github.com/dalathegreat/BYD-Battery-Emulator-For-Gen24/wiki */ -#define MODBUS_BYD //Enable this line to emulate a "BYD 11kWh HVM battery" over Modbus RTU -//#define CAN_BYD //Enable this line to emulate a "BYD Battery-Box Premium HVS" over CAN Bus -//#define SOLAX_CAN //Enable this line to emulate a "SolaX Triple Power LFP" over CAN bus -//#define PYLON_CAN //Enable this line to emulate a "Pylontech battery" over CAN bus - /* Do not change any code below this line unless you are sure what you are doing */ /* Only change battery specific settings and limits in their respective .h files */ - #include #include "HardwareSerial.h" #include "config.h" @@ -27,12 +11,21 @@ #include "Adafruit_NeoPixel.h" #include "BATTERIES.h" #include "INVERTERS.h" + +#ifdef DUAL_CAN + #include + static const uint32_t QUARTZ_FREQUENCY = 8UL * 1000UL * 1000UL ; // 8 MHz + ACAN2515 can(MCP2515_CS, SPI, MCP2515_INT); + static ACAN2515_Buffer16 gBuffer; +#endif + //CAN parameters +#define MAX_CAN_FAILURES 5000 //Amount of malformed CAN messages to allow before raising a warning CAN_device_t CAN_cfg; // CAN Config const int rx_queue_size = 10; // Receive Queue size //Interval settings -const int intervalInverterTask = 4800; //Interval at which to refresh modbus registers / inverter values +const int intervalInverterTask = 800; //Interval at which to refresh modbus registers / inverter values const int interval10 = 10; //Interval for 10ms tasks unsigned long previousMillis10ms = 50; @@ -72,6 +65,8 @@ uint16_t temperature_min = 60; //reads from battery later uint16_t bms_char_dis_status; //0 idle, 1 discharging, 2, charging uint16_t bms_status = ACTIVE; //ACTIVE - [0..5]<>[STANDBY,INACTIVE,DARKSTART,ACTIVE,FAULT,UPDATING] uint16_t stat_batt_power = 0; //power going in/out of battery +uint16_t cell_max_voltage = 3700; //Stores the highest cell voltage value in the system +uint16_t cell_min_voltage = 3700; //Stores the minimum cell voltage value in the system // Create a ModbusRTU server instance listening on Serial2 with 2000ms timeout ModbusServerRTU MBserver(Serial2, 2000); @@ -83,12 +78,12 @@ ModbusServerRTU MBserver(Serial2, 2000); Adafruit_NeoPixel pixels(1, WS2812_PIN, NEO_GRB + NEO_KHZ800); static uint8_t brightness = 0; static bool rampUp = true; -const uint8_t maxBrightness = 255; +const uint8_t maxBrightness = 100; uint8_t LEDcolor = GREEN; //Contactor parameters enum State { - WAITING_FOR_BATTERY, + DISCONNECTED, PRECHARGE, NEGATIVE, POSITIVE, @@ -96,19 +91,32 @@ enum State { COMPLETED, SHUTDOWN_REQUESTED }; -State contactorStatus = WAITING_FOR_BATTERY; +State contactorStatus = DISCONNECTED; + #define PRECHARGE_TIME_MS 160 #define NEGATIVE_CONTACTOR_TIME_MS 1000 #define POSITIVE_CONTACTOR_TIME_MS 2000 +#define PWM_Freq 20000 // 20 kHz frequency, beyond audible range +#define PWM_Res 10 // 10 Bit resolution 0 to 1023, maps 'nicely' to 0% 100% +#define PWM_Hold_Duty 250 +#define POSITIVE_PWM_Ch 0 +#define NEGATIVE_PWM_Ch 1 unsigned long prechargeStartTime = 0; unsigned long negativeStartTime = 0; unsigned long timeSpentInFaultedMode = 0; uint8_t batteryAllowsContactorClosing = 0; -uint8_t inverterAllowsContactorClosing = 1; //Startup with always allowing closing from inverter side. Only a few inverters disallow it +uint8_t inverterAllowsContactorClosing = 0; // Setup() - initialization happens here void setup() { + // Init Serial monitor + Serial.begin(9600); + while (!Serial) + { + } + Serial.println("__ OK __"); + //CAN pins pinMode(CAN_SE_PIN, OUTPUT); digitalWrite(CAN_SE_PIN, LOW); @@ -120,20 +128,29 @@ void setup() ESP32Can.CANInit(); Serial.println(CAN_cfg.speed); + #ifdef DUAL_CAN + gBuffer.initWithSize(25); + SPI.begin(MCP2515_SCK, MCP2515_MISO, MCP2515_MOSI); + Serial.println ("Configure ACAN2515") ; + ACAN2515Settings settings (QUARTZ_FREQUENCY, 500UL * 1000UL) ; // CAN bit rate 500 kb/s + settings.mRequestedMode = ACAN2515Settings::NormalMode ; // Select loopback mode + can.begin (settings, [] { can.isr (); }); + #endif + //Init contactor pins - pinMode(POSITIVE_CONTACTOR_PIN, OUTPUT); - digitalWrite(POSITIVE_CONTACTOR_PIN, LOW); - pinMode(NEGATIVE_CONTACTOR_PIN, OUTPUT); - digitalWrite(NEGATIVE_CONTACTOR_PIN, LOW); + + ledcAttachPin(POSITIVE_CONTACTOR_PIN, POSITIVE_PWM_Ch); // Attach Positive Contactor Pin to Hardware PWM Channel + ledcAttachPin(NEGATIVE_CONTACTOR_PIN, NEGATIVE_PWM_Ch); // Attach Positive Contactor Pin to Hardware PWM Channel + + ledcSetup(POSITIVE_PWM_Ch, PWM_Freq, PWM_Res); // Setup PWM Channel Frequency and Resolution + ledcSetup(NEGATIVE_PWM_Ch, PWM_Freq, PWM_Res); // Setup PWM Channel Frequency and Resolution + + ledcWrite(POSITIVE_PWM_Ch, 0); // Set Positive PWM to 0% + ledcWrite(NEGATIVE_PWM_Ch, 0); // Set Negative PWM to 0% + pinMode(PRECHARGE_PIN, OUTPUT); digitalWrite(PRECHARGE_PIN, LOW); - // Init Serial monitor - Serial.begin(9600); - while (!Serial) - { - } - Serial.println("__ OK __"); //Set up Modbus RTU Server pinMode(RS485_EN_PIN, OUTPUT); @@ -166,42 +183,33 @@ void setup() pixels.setPixelColor(0, pixels.Color(0, 0, 255)); // Blue LED full brightness while battery and CAN is starting. pixels.show(); // Incase of crash due to CAN polarity / termination, LED will remain BLUE - //Inverter Setup - #ifdef SOLAX_CAN - inverterAllowsContactorClosing = 0; //The inverter needs to allow first! - Serial.println("SOLAX CAN protocol selected"); - #endif - #ifdef MODBUS_BYD - Serial.println("BYD Modbus RTU protocol selected"); - #endif - #ifdef CAN_BYD - Serial.println("BYD CAN protocol selected"); - #endif //Inform user what setup is used + #ifdef DUAL_CAN + Serial.println("Dual CAN Bus (ESP32+MCP2515) selected"); + #endif + #ifdef BATTERY_TYPE_LEAF Serial.println("Nissan LEAF battery selected"); - #endif + #endif #ifdef TESLA_MODEL_3_BATTERY Serial.println("Tesla Model 3 battery selected"); #endif #ifdef RENAULT_ZOE_BATTERY Serial.println("Renault Zoe / Kangoo battery selected"); - #endif - #ifdef BMW_I3_BATTERY - Serial.println("BMW i3 battery selected"); - #endif + #endif #ifdef IMIEV_ION_CZERO_BATTERY Serial.println("Mitsubishi i-MiEV / Citroen C-Zero / Peugeot Ion battery selected"); #endif - #ifdef KIA_HYUNDAI_64_BATTERY - Serial.println("Kia Niro / Hyundai Kona 64kWh battery selected"); - #endif } // perform main program functions void loop() { handle_can(); //runs as fast as possible, handle CAN routines + + #ifdef DUAL_CAN + handle_can2(); + #endif if (millis() - previousMillis10ms >= interval10) //every 10ms { @@ -235,15 +243,9 @@ void handle_can() #ifdef RENAULT_ZOE_BATTERY receive_can_zoe_battery(rx_frame); #endif - #ifdef BMW_I3_BATTERY - receive_can_i3_battery(rx_frame); - #endif #ifdef IMIEV_ION_CZERO_BATTERY receive_can_imiev_battery(rx_frame); #endif - #ifdef KIA_HYUNDAI_64_BATTERY - receive_can_kiaHyundai_64_battery(rx_frame); - #endif #ifdef CAN_BYD receive_can_byd(rx_frame); #endif @@ -258,9 +260,9 @@ void handle_can() #ifdef SOLAX_CAN receive_can_solax(rx_frame); #endif - #ifdef PYLON_CAN - receive_can_pylon(rx_frame); - #endif + #ifdef PYLON_CAN + receive_can_pylon(rx_frame); + #endif } } //When we are done checking if a CAN message has arrived, we can focus on sending CAN messages @@ -281,40 +283,76 @@ void handle_can() #ifdef RENAULT_ZOE_BATTERY send_can_zoe_battery(); #endif - #ifdef BMW_I3_BATTERY - send_can_i3_battery(); - #endif #ifdef IMIEV_ION_CZERO_BATTERY send_can_imiev_battery(); #endif - #ifdef KIA_HYUNDAI_64_BATTERY - send_can_kiaHyundai_64_battery(); - #endif #ifdef CHADEMO send_can_chademo_battery(); #endif } +#ifdef DUAL_CAN + void handle_can2() +{ //This function is similar to handle_can, but just takes care of inverters in the 2nd bus. + //Depending on which inverter is selected, we forward this to their respective CAN routines + CAN_frame_t rx_frame2; //Struct with ESP32Can library format, compatible with the rest of the program + CANMessage MCP2515Frame; //Struct with ACAN2515 library format, needed to use thw MCP2515 library + + if ( can.available() ) + { + can.receive(MCP2515Frame); + + rx_frame2.MsgID = MCP2515Frame.id; + rx_frame2.FIR.B.FF = MCP2515Frame.ext ? CAN_frame_ext : CAN_frame_std; + rx_frame2.FIR.B.RTR = MCP2515Frame.rtr ? CAN_RTR : CAN_no_RTR; + rx_frame2.FIR.B.DLC = MCP2515Frame.len; + for (uint8_t i=0 ; i 1500) + if(timeSpentInFaultedMode > 500) { contactorStatus = SHUTDOWN_REQUESTED; } @@ -353,8 +391,12 @@ void handle_contactors() } //After that, check if we are OK to start turning on the battery - if(contactorStatus == WAITING_FOR_BATTERY) + if(contactorStatus == DISCONNECTED) { + digitalWrite(PRECHARGE_PIN, LOW); + ledcWrite(POSITIVE_PWM_Ch, 0); + ledcWrite(NEGATIVE_PWM_Ch, 0); + if(batteryAllowsContactorClosing && inverterAllowsContactorClosing) { contactorStatus = PRECHARGE; @@ -362,7 +404,9 @@ void handle_contactors() } if(contactorStatus == COMPLETED) - { //Skip running the state machine below if it has already completed + { + if (!inverterAllowsContactorClosing) contactorStatus = DISCONNECTED; + //Skip running the state machine below if it has already completed return; } @@ -377,7 +421,7 @@ void handle_contactors() case NEGATIVE: if (currentTime - prechargeStartTime >= PRECHARGE_TIME_MS) { - digitalWrite(NEGATIVE_CONTACTOR_PIN, HIGH); + ledcWrite(NEGATIVE_PWM_Ch, 1023); negativeStartTime = currentTime; contactorStatus = POSITIVE; } @@ -385,7 +429,7 @@ void handle_contactors() case POSITIVE: if (currentTime - negativeStartTime >= NEGATIVE_CONTACTOR_TIME_MS) { - digitalWrite(POSITIVE_CONTACTOR_PIN, HIGH); + ledcWrite(POSITIVE_PWM_Ch, 1023); contactorStatus = PRECHARGE_OFF; } break; @@ -393,6 +437,8 @@ void handle_contactors() case PRECHARGE_OFF: if (currentTime - negativeStartTime >= POSITIVE_CONTACTOR_TIME_MS) { digitalWrite(PRECHARGE_PIN, LOW); + ledcWrite(NEGATIVE_PWM_Ch, PWM_Hold_Duty); + ledcWrite(POSITIVE_PWM_Ch, PWM_Hold_Duty); contactorStatus = COMPLETED; } break; @@ -520,4 +566,4 @@ void handle_LED_state() } pixels.show(); // This sends the updated pixel color to the hardware. -} +} \ No newline at end of file diff --git a/Software/config.h b/Software/config.h index d8ec155e..b1787f70 100644 --- a/Software/config.h +++ b/Software/config.h @@ -1,6 +1,21 @@ #ifndef __CONFIG_H__ #define __CONFIG_H__ +/* Select battery used */ +#define BATTERY_TYPE_LEAF // See NISSAN-LEAF-BATTERY.h for more LEAF battery settings +//#define TESLA_MODEL_3_BATTERY // See TESLA-MODEL-3-BATTERY.h for more Tesla battery settings +//#define RENAULT_ZOE_BATTERY // See RENAULT-ZOE-BATTERY.h for more Zoe battery settings +//#define IMIEV_ION_CZERO_BATTERY // See IMIEV-CZERO-ION-BATTERY.h for more triplet battery settings +//#define CHADEMO // See CHADEMO.h for more Chademo related settings + +/* Select inverter communication protocol. See Wiki for which to use with your inverter: https://github.com/dalathegreat/BYD-Battery-Emulator-For-Gen24/wiki */ +//#define MODBUS_BYD //Enable this line to emulate a "BYD 11kWh HVM battery" over Modbus RTU +//#define CAN_BYD //Enable this line to emulate a "BYD Battery-Box Premium HVS" over CAN Bus +#define SOLAX_CAN //Enable this line to emulate a "SolaX Triple Power LFP" over CAN bus +//#define PYLON_CAN //Enable this line to emulate a "Pylontech battery" over CAN bus + +#define DUAL_CAN //Enable this line to activate an isolated secondary CAN Bus using MCP2515 controller (Needed for FoxESS inverts) + // PIN #define PIN_5V_EN 16 @@ -8,6 +23,12 @@ #define CAN_RX_PIN 26 #define CAN_SE_PIN 23 +#define MCP2515_SCK 12 // SCK input of MCP2515 +#define MCP2515_MOSI 5 // SDI input of MCP2515 +#define MCP2515_MISO 34 // SDO output of MCP2515 | Pin 34 is input only, without pullup/down resistors +#define MCP2515_CS 18 // CS input of MCP2515 +#define MCP2515_INT 35 // INT output of MCP2515 | | Pin 35 is input only, without pullup/down resistors + #define RS485_EN_PIN 17 // 17 /RE #define RS485_TX_PIN 22 // 21 #define RS485_RX_PIN 21 // 22