mirror of
https://github.com/dalathegreat/Battery-Emulator.git
synced 2025-10-05 02:39:57 +02:00
289 lines
11 KiB
C++
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)
|
|
}
|