Battery-Emulator/Software/src/communication/contactorcontrol/comm_contactorcontrol.cpp
2025-06-12 23:26:43 +03:00

289 lines
11 KiB
C++

#include "comm_contactorcontrol.h"
#include "../../include.h"
// Parameters
#ifndef CONTACTOR_CONTROL
#ifdef PWM_CONTACTOR_CONTROL
#error CONTACTOR_CONTROL needs to be enabled for PWM_CONTACTOR_CONTROL
#endif
#endif
#ifdef CONTACTOR_CONTROL
enum State { DISCONNECTED, START_PRECHARGE, PRECHARGE, POSITIVE, PRECHARGE_OFF, COMPLETED, SHUTDOWN_REQUESTED };
State contactorStatus = DISCONNECTED;
#define ON 1
#define 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
#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
unsigned long prechargeStartTime = 0;
unsigned long negativeStartTime = 0;
unsigned long prechargeCompletedTime = 0;
unsigned long timeSpentInFaultedMode = 0;
#endif
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 powerRemovalDuration = 30000; // 30 seconds in milliseconds
const unsigned long bmsWarmupDuration = 3000;
void set(uint8_t pin, bool direction, uint32_t pwm_freq = 0xFFFF) {
#ifdef PWM_CONTACTOR_CONTROL
if (pwm_freq != 0xFFFF) {
ledcWrite(pin, pwm_freq);
return;
}
#endif
if (direction == 1) {
digitalWrite(pin, HIGH);
} else { // 0
digitalWrite(pin, LOW);
}
}
// Initialization functions
void init_contactors() {
// Init contactor pins
#ifdef CONTACTOR_CONTROL
#ifdef PWM_CONTACTOR_CONTROL
// Setup PWM Channel Frequency and Resolution
ledcAttachChannel(POSITIVE_CONTACTOR_PIN, PWM_Freq, PWM_Res, PWM_Positive_Channel);
ledcAttachChannel(NEGATIVE_CONTACTOR_PIN, PWM_Freq, PWM_Res, PWM_Negative_Channel);
// Set all pins OFF (0% PWM)
ledcWrite(POSITIVE_CONTACTOR_PIN, PWM_OFF_DUTY);
ledcWrite(NEGATIVE_CONTACTOR_PIN, PWM_OFF_DUTY);
#else //Normal CONTACTOR_CONTROL
pinMode(POSITIVE_CONTACTOR_PIN, OUTPUT);
set(POSITIVE_CONTACTOR_PIN, OFF);
pinMode(NEGATIVE_CONTACTOR_PIN, OUTPUT);
set(NEGATIVE_CONTACTOR_PIN, OFF);
#endif // Precharge never has PWM regardless of setting
pinMode(PRECHARGE_PIN, OUTPUT);
set(PRECHARGE_PIN, OFF);
#endif // CONTACTOR_CONTROL
#ifdef CONTACTOR_CONTROL_DOUBLE_BATTERY
pinMode(SECOND_BATTERY_CONTACTORS_PIN, OUTPUT);
set(SECOND_BATTERY_CONTACTORS_PIN, OFF);
#endif // CONTACTOR_CONTROL_DOUBLE_BATTERY
// Init BMS contactor
#if defined HW_STARK || defined HW_3LB // This hardware has dedicated pin, always enable on start
pinMode(BMS_POWER, OUTPUT); //LilyGo is omitted from this, only enabled if user selects PERIODIC_BMS_RESET
digitalWrite(BMS_POWER, HIGH);
#endif // HW with dedicated BMS pins
#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET) // User has enabled BMS reset, turn on output on start
pinMode(BMS_POWER, OUTPUT);
digitalWrite(BMS_POWER, HIGH);
#endif //PERIODIC_BMS_RESET
}
static void dbg_contactors(const char* state) {
#ifdef DEBUG_LOG
logging.print("[");
logging.print(millis());
logging.print(" ms] contactors control: ");
logging.println(state);
#endif
}
// 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 defined(SMA_BYD_H_CAN) || defined(SMA_BYD_HVS_CAN) || defined(SMA_TRIPOWER_CAN)
datalayer.system.status.inverter_allows_contactor_closing = digitalRead(INVERTER_CONTACTOR_ENABLE_PIN);
#endif
handle_BMSpower(); // Some batteries need to be periodically power cycled
#ifdef CONTACTOR_CONTROL_DOUBLE_BATTERY
handle_contactors_battery2();
#endif // CONTACTOR_CONTROL_DOUBLE_BATTERY
#ifdef CONTACTOR_CONTROL
// 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(PRECHARGE_PIN, OFF);
set(NEGATIVE_CONTACTOR_PIN, OFF, PWM_OFF_DUTY);
set(POSITIVE_CONTACTOR_PIN, OFF, PWM_OFF_DUTY);
set_event(EVENT_ERROR_OPEN_CONTACTOR, 0);
datalayer.system.status.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 battery
if (contactorStatus == DISCONNECTED) {
set(PRECHARGE_PIN, OFF);
set(NEGATIVE_CONTACTOR_PIN, OFF, PWM_OFF_DUTY);
set(POSITIVE_CONTACTOR_PIN, OFF, PWM_OFF_DUTY);
datalayer.system.status.contactors_engaged = false;
if (datalayer.system.status.battery_allows_contactor_closing &&
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(NEGATIVE_CONTACTOR_PIN, ON, PWM_ON_DUTY);
dbg_contactors("NEGATIVE");
prechargeStartTime = currentTime;
contactorStatus = PRECHARGE;
break;
case PRECHARGE:
if (currentTime - prechargeStartTime >= NEGATIVE_CONTACTOR_TIME_MS) {
set(PRECHARGE_PIN, ON);
dbg_contactors("PRECHARGE");
negativeStartTime = currentTime;
contactorStatus = POSITIVE;
}
break;
case POSITIVE:
if (currentTime - negativeStartTime >= PRECHARGE_TIME_MS) {
set(POSITIVE_CONTACTOR_PIN, ON, PWM_ON_DUTY);
dbg_contactors("POSITIVE");
prechargeCompletedTime = currentTime;
contactorStatus = PRECHARGE_OFF;
}
break;
case PRECHARGE_OFF:
if (currentTime - prechargeCompletedTime >= PRECHARGE_COMPLETED_TIME_MS) {
set(PRECHARGE_PIN, OFF);
set(NEGATIVE_CONTACTOR_PIN, ON, PWM_HOLD_DUTY);
set(POSITIVE_CONTACTOR_PIN, ON, PWM_HOLD_DUTY);
dbg_contactors("PRECHARGE_OFF");
contactorStatus = COMPLETED;
datalayer.system.status.contactors_engaged = true;
}
break;
default:
break;
}
#endif // CONTACTOR_CONTROL
}
#ifdef CONTACTOR_CONTROL_DOUBLE_BATTERY
void handle_contactors_battery2() {
if ((contactorStatus == COMPLETED) && datalayer.system.status.battery2_allowed_contactor_closing) {
set(SECOND_BATTERY_CONTACTORS_PIN, ON);
datalayer.system.status.contactors_battery2_engaged = true;
} else { // Closing contactors on secondary battery not allowed
set(SECOND_BATTERY_CONTACTORS_PIN, OFF);
datalayer.system.status.contactors_battery2_engaged = false;
}
}
#endif // CONTACTOR_CONTROL_DOUBLE_BATTERY
/* 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 defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET)
// Get current time
currentTime = millis();
#ifdef PERIODIC_BMS_RESET
// Check if 24 hours have passed since the last power removal
if ((currentTime + bmsResetTimeOffset) - lastPowerRemovalTime >= powerRemovalInterval) {
start_bms_reset();
}
#endif //PERIODIC_BMS_RESET
// If power has been removed for 30 seconds, restore the power
if (datalayer.system.status.BMS_reset_in_progress && currentTime - lastPowerRemovalTime >= powerRemovalDuration) {
// Reapply power to the BMS
digitalWrite(BMS_POWER, HIGH);
#ifdef BMS_2_POWER
digitalWrite(BMS_2_POWER, HIGH); // Same for battery 2
#endif
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);
}
#endif //defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET)
}
void start_bms_reset() {
#if defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET)
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, LOW); // Remove power by setting the BMS power pin to LOW
#ifdef BMS_2_POWER
digitalWrite(BMS_2_POWER, LOW); // Same for battery 2
#endif
}
#endif //defined(PERIODIC_BMS_RESET) || defined(REMOTE_BMS_RESET)
}