#include "comm_contactorcontrol.h" #include "../../devboard/hal/hal.h" #include "../../devboard/safety/safety.h" #include "../../inverter/INVERTERS.h" #ifdef CONTACTOR_CONTROL const bool contactor_control_enabled_default = true; #else const bool contactor_control_enabled_default = false; #endif bool contactor_control_enabled = contactor_control_enabled_default; #ifdef PWM_CONTACTOR_CONTROL const bool pwn_contactor_control_default = true; #else const bool pwn_contactor_control_default = false; #endif bool pwm_contactor_control = pwn_contactor_control_default; #ifdef PERIODIC_BMS_RESET const bool periodic_bms_reset_default = true; #else const bool periodic_bms_reset_default = false; #endif bool periodic_bms_reset = periodic_bms_reset_default; #ifdef REMOTE_BMS_RESET const bool remote_bms_reset_default = true; #else const bool remote_bms_reset_default = false; #endif bool remote_bms_reset = remote_bms_reset_default; #ifdef CONTACTOR_CONTROL_DOUBLE_BATTERY const bool contactor_control_enabled_double_battery_default = true; #else const bool contactor_control_enabled_double_battery_default = false; #endif bool contactor_control_enabled_double_battery = contactor_control_enabled_double_battery_default; // TODO: Ensure valid values at run-time // Parameters enum State { DISCONNECTED, START_PRECHARGE, PRECHARGE, POSITIVE, PRECHARGE_OFF, COMPLETED, SHUTDOWN_REQUESTED }; State contactorStatus = DISCONNECTED; const int ON = 1; const int OFF = 0; #ifdef NC_CONTACTORS //Normally closed contactors use inverted logic #undef ON #define ON 0 #undef OFF #define OFF 1 #endif //NC_CONTACTORS #define MAX_ALLOWED_FAULT_TICKS 1000 //1000 = 10 seconds #define NEGATIVE_CONTACTOR_TIME_MS \ 500 // Time after negative contactor is turned on, to start precharge (not actual precharge time!) #define PRECHARGE_COMPLETED_TIME_MS \ 1000 // After successful precharge, resistor is turned off after this delay (and contactors are economized if PWM enabled) #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 PWM_OFF_DUTY 0 #define PWM_ON_DUTY 1023 #define PWM_Positive_Channel 0 #define PWM_Negative_Channel 1 static unsigned long prechargeStartTime = 0; unsigned long negativeStartTime = 0; unsigned long prechargeCompletedTime = 0; unsigned long timeSpentInFaultedMode = 0; unsigned long currentTime = 0; unsigned long lastPowerRemovalTime = 0; unsigned long bmsPowerOnTime = 0; const unsigned long powerRemovalInterval = 24 * 60 * 60 * 1000; // 24 hours in milliseconds const unsigned long bmsWarmupDuration = 3000; void set(uint8_t pin, bool direction, uint32_t pwm_freq = 0xFFFF) { if (pwm_contactor_control) { if (pwm_freq != 0xFFFF) { ledcWrite(pin, pwm_freq); return; } } if (direction == 1) { digitalWrite(pin, HIGH); } else { // 0 digitalWrite(pin, LOW); } } // Initialization functions const char* contactors = "Contactors"; bool init_contactors() { // Init contactor pins if (contactor_control_enabled) { auto posPin = esp32hal->POSITIVE_CONTACTOR_PIN(); auto negPin = esp32hal->NEGATIVE_CONTACTOR_PIN(); auto precPin = esp32hal->PRECHARGE_PIN(); if (!esp32hal->alloc_pins(contactors, posPin, negPin, precPin)) { return false; } if (pwm_contactor_control) { // Setup PWM Channel Frequency and Resolution ledcAttachChannel(posPin, PWM_Freq, PWM_Res, PWM_Positive_Channel); ledcAttachChannel(negPin, PWM_Freq, PWM_Res, PWM_Negative_Channel); // Set all pins OFF (0% PWM) ledcWrite(posPin, PWM_OFF_DUTY); ledcWrite(negPin, PWM_OFF_DUTY); } else { //Normal CONTACTOR_CONTROL pinMode(posPin, OUTPUT); set(posPin, OFF); pinMode(negPin, OUTPUT); set(negPin, OFF); } // Precharge never has PWM regardless of setting pinMode(precPin, OUTPUT); set(precPin, OFF); } if (contactor_control_enabled_double_battery) { auto second_contactors = esp32hal->SECOND_BATTERY_CONTACTORS_PIN(); if (!esp32hal->alloc_pins(contactors, second_contactors)) { return false; } pinMode(second_contactors, OUTPUT); set(second_contactors, OFF); } // Init BMS contactor if (periodic_bms_reset || remote_bms_reset || esp32hal->always_enable_bms_power()) { auto pin = esp32hal->BMS_POWER(); if (!esp32hal->alloc_pins("BMS power", pin)) { return false; } pinMode(pin, OUTPUT); digitalWrite(pin, HIGH); } return true; } static void dbg_contactors(const char* state) { logging.print("["); logging.print(millis()); logging.print(" ms] contactors control: "); logging.println(state); } // Main functions of the handle_contactors include checking if inverter allows for closing, checking battery 2, checking BMS power output, and actual contactor closing/precharge via GPIO void handle_contactors() { if (inverter && inverter->controls_contactor()) { datalayer.system.status.inverter_allows_contactor_closing = inverter->allows_contactor_closing(); } auto posPin = esp32hal->POSITIVE_CONTACTOR_PIN(); auto negPin = esp32hal->NEGATIVE_CONTACTOR_PIN(); auto prechargePin = esp32hal->PRECHARGE_PIN(); auto bms_power_pin = esp32hal->BMS_POWER(); if (bms_power_pin != GPIO_NUM_NC) { handle_BMSpower(); // Some batteries need to be periodically power cycled } if (contactor_control_enabled_double_battery) { handle_contactors_battery2(); } if (contactor_control_enabled) { // 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; } if (contactorStatus == SHUTDOWN_REQUESTED) { set(prechargePin, OFF); set(negPin, OFF, PWM_OFF_DUTY); set(posPin, OFF, PWM_OFF_DUTY); set_event(EVENT_ERROR_OPEN_CONTACTOR, 0); datalayer.system.status.contactors_engaged = 2; 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 battery if (contactorStatus == DISCONNECTED) { set(prechargePin, OFF); set(negPin, OFF, PWM_OFF_DUTY); set(posPin, OFF, PWM_OFF_DUTY); datalayer.system.status.contactors_engaged = 0; if (datalayer.system.status.inverter_allows_contactor_closing && !datalayer.system.settings.equipment_stop_active) { contactorStatus = START_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; } // Skip running the state machine below if it has already completed return; } currentTime = millis(); if (currentTime < INTERVAL_10_S) { // Skip running the state machine before system has started up. // Gives the system some time to detect any faults from battery before blindly just engaging the contactors return; } // Handle actual state machine. This first turns on Negative, then Precharge, then Positive, and finally turns OFF precharge switch (contactorStatus) { case START_PRECHARGE: set(negPin, ON, PWM_ON_DUTY); dbg_contactors("NEGATIVE"); prechargeStartTime = currentTime; contactorStatus = PRECHARGE; break; case PRECHARGE: if (currentTime - prechargeStartTime >= NEGATIVE_CONTACTOR_TIME_MS) { set(prechargePin, ON); dbg_contactors("PRECHARGE"); negativeStartTime = currentTime; contactorStatus = POSITIVE; } break; case POSITIVE: if (currentTime - negativeStartTime >= PRECHARGE_TIME_MS) { set(posPin, ON, PWM_ON_DUTY); dbg_contactors("POSITIVE"); prechargeCompletedTime = currentTime; contactorStatus = PRECHARGE_OFF; } break; case PRECHARGE_OFF: if (currentTime - prechargeCompletedTime >= PRECHARGE_COMPLETED_TIME_MS) { set(prechargePin, OFF); set(negPin, ON, PWM_HOLD_DUTY); set(posPin, ON, PWM_HOLD_DUTY); dbg_contactors("PRECHARGE_OFF"); contactorStatus = COMPLETED; datalayer.system.status.contactors_engaged = 1; } break; default: break; } } } void handle_contactors_battery2() { auto second_contactors = esp32hal->SECOND_BATTERY_CONTACTORS_PIN(); if ((contactorStatus == COMPLETED) && datalayer.system.status.battery2_allowed_contactor_closing) { set(second_contactors, ON); datalayer.system.status.contactors_battery2_engaged = true; } else { // Closing contactors on secondary battery not allowed set(second_contactors, OFF); datalayer.system.status.contactors_battery2_engaged = false; } } /* PERIODIC_BMS_RESET - Once every 24 hours we remove power from the BMS_power pin for 30 seconds. REMOTE_BMS_RESET - Allows the user to remotely powercycle the BMS by sending a command to the emulator via MQTT. This makes the BMS recalculate all SOC% and avoid memory leaks During that time we also set the emulator state to paused in order to not try and send CAN messages towards the battery Feature is only used if user has enabled PERIODIC_BMS_RESET in the USER_SETTINGS */ void handle_BMSpower() { if (periodic_bms_reset || remote_bms_reset) { auto bms_power_pin = esp32hal->BMS_POWER(); // Get current time currentTime = millis(); if (periodic_bms_reset) { // Check if 24 hours have passed since the last power removal if ((currentTime + bmsResetTimeOffset) - lastPowerRemovalTime >= powerRemovalInterval) { start_bms_reset(); } } // If power has been removed for user configured interval (1-59 seconds), restore the power if (datalayer.system.status.BMS_reset_in_progress && currentTime - lastPowerRemovalTime >= datalayer.battery.settings.user_set_bms_reset_duration_ms) { // Reapply power to the BMS digitalWrite(bms_power_pin, HIGH); bmsPowerOnTime = currentTime; datalayer.system.status.BMS_reset_in_progress = false; // Reset the power removal flag datalayer.system.status.BMS_startup_in_progress = true; // Set the BMS warmup flag } //if power has been restored we need to wait a couple of seconds to unpause the battery if (datalayer.system.status.BMS_startup_in_progress && currentTime - bmsPowerOnTime >= bmsWarmupDuration) { setBatteryPause(false, false, false, false); datalayer.system.status.BMS_startup_in_progress = false; // Reset the BMS warmup removal flag set_event(EVENT_PERIODIC_BMS_RESET, 0); } } } void start_bms_reset() { if (periodic_bms_reset || remote_bms_reset) { auto bms_power_pin = esp32hal->BMS_POWER(); if (!datalayer.system.status.BMS_reset_in_progress) { lastPowerRemovalTime = currentTime; // Record the time when BMS reset was started // we are now resetting at the correct time. We don't need to offset anymore bmsResetTimeOffset = 0; // Set a flag to let the rest of the system know we are cutting power to the BMS. // The battery CAN sending routine will then know not to try guto send anything towards battery while active datalayer.system.status.BMS_reset_in_progress = true; // Set emulator state to paused (Max Charge/Discharge = 0 & CAN = stop) // We try to keep contactors engaged during this pause, and just ramp power down to 0. setBatteryPause(true, false, false, false); digitalWrite(bms_power_pin, LOW); // Remove power by setting the BMS power pin to LOW } } }