diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index c560fe8..374fce5 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -19,13 +19,19 @@ namespace constants { static_assert(kBoardThermistorIndex == kMonitoredThermistorsPerModule, "Board thermistor should be the first thermistor excluded from aggregate status"); - constexpr uint32_t kPollIntervalMs = 500; + // interval at which the BMS pulls data from the latest slave board readings + constexpr uint32_t kPollIntervalMs = 100; + // interval BMS sends data via Serial constexpr uint32_t kLogIntervalMs = 2000; - constexpr uint32_t kModuleScanIntervalMs = 2000; + // interval BMS scans modules + constexpr uint32_t kModuleScanIntervalMs = 500; constexpr uint32_t kCanBitRate = 250000; - constexpr uint32_t kCanStatusIntervalMs = 500; - constexpr uint32_t kCanStatusMessageId = 0x070; + constexpr uint32_t kCanStatusIntervalMs = 1000; // Rate at which data CAN status information is sent + constexpr uint32_t kCanChargerIntervalMs = 1000; // Rate to check if the charger sent a CAN message to toggle on charging mode + constexpr uint32_t kCanChargerTimeOutMs = 2000; // If the charger stops sending CAN messages then toggle off charging mode + constexpr uint32_t kCanChargerControlIntervalMs = 1000; // Rate at which the charger needs a CAN control message + constexpr uint32_t kChargerStatusUpdateIntervalMs = 200; // Rate at which charge states are updated based on BMS polled data constexpr uint8_t kCanStatusPayloadLength = 8; constexpr uint8_t kConnectDebounce = 2; @@ -44,10 +50,18 @@ namespace constants { constexpr float kTempWarningMinC = 5.0f; constexpr float kTempGoodMaxC = 50.0f; - constexpr float kTempWarningMaxC = 60.0f; - constexpr float kTempExhaustedMaxC = 70.0f; + constexpr uint8_t kTempChargingFaultC = 60; // stop charging if max cell temp reaches this value + constexpr float kTempWarningMaxC = 60.0f; // if cells reach 60 C then the BMS should error + constexpr float kTempExhaustedMaxC = 70.0f; // the cells should not go any higher than this per the datasheet constexpr float kBoardThermistorFaultMinC = 70.0f; constexpr std::size_t kLedCount = 13; constexpr uint8_t kLedBrightness = 32; -} // namespace constants + + // constants for charging + constexpr uint16_t kChargerPinConsideredHigh = 620; // Charger analog read pin will read 1 V when high, thus 4096/3.3 V = 1241, and we add space for uncertainty and noise + constexpr uint16_t kVoltageChargerMaxPackV = 445; // Charger will shutoff if limit is over-reached and BMS or wiring fails; this parameter is sent to the charger via CAN in charger mode + constexpr float kMaxChargerPowerOutputW = 6550.0f; // ELCON charger specification + constexpr uint16_t kStartChargeA = 1; // 0.5C charging amperage + +} // namespace constants diff --git a/include/PINS.h b/include/PINS.h index e258e9b..6c2169e 100644 --- a/include/PINS.h +++ b/include/PINS.h @@ -18,9 +18,7 @@ #define ADBMS_SPI_SCLK 10 #define ADBMS_SPI_MOSI 11 #define ADBMS_SPI_MISO 12 -#define CHARGE_ENABLE_SENSE 13 #define DRIVE_ENABLE_SENSE 14 -#define BMS_STATUS_OUTPUT 15 #define UART0_TX 16 #define UART0_RX 17 #define CAN0_INT 18 @@ -33,7 +31,11 @@ #define GPIO_25 25 #define GPIO_26 26 #define GPIO_27 27 -#define GPIO_28 28 -#define GPIO_29 29 + +// pin out for shutdown circut +#define BMS_STATUS_OUTPUT 28 + +// pin to enable or disable charging +#define CHARGE_ENABLE_SENSE 29 #endif //PINS_H diff --git a/include/SOCLookUpTable.h b/include/SOCLookUpTable.h new file mode 100644 index 0000000..c1927bf --- /dev/null +++ b/include/SOCLookUpTable.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include + +// SOC : State of charge +// Human readable state of the accumulator because the charging logic +// uses only the min cell voltage or the max cell voltage in the pack + +namespace soclookuptable { + constexpr uint8_t kNumLookUpPoints = 100; + + constexpr float kVoltageTable[kNumLookUpPoints] = { + 3150.0f, 3159.6f, 3169.2f, 3178.8f, 3188.4f, 3198.0f, 3207.6f, 3217.2f, + 3226.8f, 3236.4f, 3246.0f, 3255.6f, 3265.2f, 3274.7f, 3284.3f, 3293.9f, + 3303.5f, 3313.1f, 3322.7f, 3332.3f, 3341.9f, 3351.5f, 3361.1f, 3370.7f, + 3380.3f, 3389.9f, 3399.5f, 3409.1f, 3418.7f, 3428.3f, 3437.9f, 3447.5f, + 3457.1f, 3466.7f, 3476.3f, 3485.9f, 3495.5f, 3505.1f, 3514.6f, 3524.2f, + 3533.8f, 3543.4f, 3553.0f, 3562.6f, 3572.2f, 3581.8f, 3591.4f, 3601.0f, + 3610.6f, 3620.2f, 3629.8f, 3639.4f, 3649.0f, 3658.6f, 3668.2f, 3677.8f, + 3687.4f, 3697.0f, 3706.6f, 3716.2f, 3725.8f, 3735.4f, 3744.9f, 3754.5f, + 3764.1f, 3773.7f, 3783.3f, 3792.9f, 3802.5f, 3812.1f, 3821.7f, 3831.3f, + 3840.9f, 3850.5f, 3860.1f, 3869.7f, 3879.3f, 3888.9f, 3898.5f, 3908.1f, + 3917.7f, 3927.3f, 3936.9f, 3946.5f, 3956.1f, 3965.7f, 3975.3f, 3984.8f, + 3994.4f, 4004.0f, 4013.6f, 4023.2f, 4032.8f, 4042.4f, 4052.0f, 4061.6f, + 4071.2f, 4080.8f, 4090.4f, 4100.0f + }; + + constexpr float kSocTable[kNumLookUpPoints] = { + 0.0f, 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.8f, 0.9f, 1.1f, + 1.3f, 1.5f, 1.7f, 1.9f, 2.1f, 2.4f, 2.7f, 2.9f, 3.3f, 3.6f, + 4.0f, 4.4f, 4.8f, 5.3f, 5.8f, 6.3f, 6.8f, 7.5f, 8.1f, 8.8f, + 9.5f, 10.3f, 11.2f, 12.0f, 13.0f, 14.0f, 15.0f, 16.2f, 17.3f, 18.6f, + 19.9f, 21.3f, 22.7f, 24.2f, 25.8f, 27.4f, 29.1f, 30.8f, 32.6f, 34.5f, + 36.4f, 38.3f, 40.3f, 42.3f, 44.4f, 46.4f, 48.5f, 50.6f, 52.7f, 54.8f, + 56.9f, 58.9f, 61.0f, 63.0f, 64.9f, 66.9f, 68.7f, 70.6f, 72.4f, 74.1f, + 75.8f, 77.4f, 78.9f, 80.4f, 81.8f, 83.1f, 84.4f, 85.6f, 86.8f, 87.9f, + 88.9f, 89.9f, 90.8f, 91.7f, 92.5f, 93.3f, 94.0f, 94.7f, 95.3f, 95.9f, + 96.5f, 97.0f, 97.5f, 97.9f, 98.3f, 98.7f, 99.1f, 99.4f, 99.7f, 100.0f, + }; + +} // State of charge lookup table + +// Maps the first table (voltages) to the second table (percentage) +float ReadBMS::lookUpSOC(uint16_t cellMv) { + // check if value is out of range + if (cellMv <= soclookuptable::kVoltageTable[0]) { + return soclookuptable::kSocTable[0]; + } + if (cellMv >= soclookuptable::kVoltageTable[soclookuptable::kNumLookUpPoints - 1]) { + return soclookuptable::kSocTable[soclookuptable::kNumLookUpPoints - 1]; + } + + // linear interpolation to map between the two tables + for (size_t i=0; i < soclookuptable::kNumLookUpPoints - 1; i++) { + if (cellMv >= soclookuptable::kVoltageTable[i] && cellMv <= soclookuptable::kVoltageTable[i+1]) { + float v1 = soclookuptable::kVoltageTable[i]; + float v2 = soclookuptable::kVoltageTable[i + 1]; + float soc1 = soclookuptable::kSocTable[i]; + float soc2 = soclookuptable::kSocTable[i + 1]; + + return static_cast((soc1 * (v1 - cellMv) + soc2 * (cellMv - v2)) / (v1 - v2)); + } + } + + // fallback + return 0.0; +} diff --git a/include/SystemStatus.h b/include/SystemStatus.h index db4bdcc..64e503c 100644 --- a/include/SystemStatus.h +++ b/include/SystemStatus.h @@ -88,15 +88,7 @@ inline StatusMode evaluateVoltageStatus(const ModuleReadings& module) { if (cellMv == adbms6830::BMSInterface::kInvalidCellValue) { return StatusMode::BAD_DATA; } - #if defined(DANGEROUS_MODE) - if (cellMv >= constants::kBalanceMaxCellMv) { - continue; - } - if (cellMv < constants::kCellVoltageErrorMinMv || - (cellMv > constants::kCellVoltageErrorMaxMv && cellMv < constants::kBalanceMaxCellMv)) { - #else if (cellMv < constants::kCellVoltageErrorMinMv || cellMv > constants::kCellVoltageErrorMaxMv) { - #endif return StatusMode::ERROR; } if (cellMv < constants::kCellVoltageExhaustedMinMv || cellMv > constants::kCellVoltageWarningMaxMv) { diff --git a/platformio.ini b/platformio.ini index c0304c9..fd6b135 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,10 +15,11 @@ framework = arduino board_build.core = earlephilhower board_build.filesystem_size = 0.5m monitor_speed = 115200 +monitor_filters = default, log2file upload_protocol = picotool -build_flags = - -DARDUINO_USB_CDC_ON_BOOT=1 -lib_deps = - fastled/FastLED@^3.10.3 - pierremolinaro/acan2517 - pierremolinaro/acan2517FD +build_flags = + -DARDUINO_USB_CDC_ON_BOOT=1 +lib_deps = + fastled/FastLED@^3.10.3 + pierremolinaro/acan2517 + pierremolinaro/acan2517FD diff --git a/src/BMSControl.cpp b/src/BMSControl.cpp index ba82e87..8d851b0 100644 --- a/src/BMSControl.cpp +++ b/src/BMSControl.cpp @@ -1,4 +1,5 @@ #include "BMSControl.h" +#include "SOCLookUpTable.h" const SPISettings ReadBMS::kBmsSpiSettings(1000000, MSBFIRST, SPI_MODE0); @@ -77,9 +78,31 @@ void ReadBMS::updateBalancing(bool enabled) { return; } + // update global pack min cell voltage + uint16_t lowestPackCellMv = UINT16_MAX; for (std::size_t moduleIndex = 0; moduleIndex < kModuleCount; ++moduleIndex) { ModuleReadings& module = pollData_.modules[moduleIndex]; - module.balanceMask = balanceMaskForModule(module, module.balanceMask); + + // pass disconnected cells for now, but we need to look into this + if (!module.connected || !module.cellDataValid) { + continue; + } + + for (uint16_t cellMv : module.cellVoltages) { + // pass invalid cells for now, but we need to look this later + if (cellMv == adbms6830::BMSInterface::kInvalidCellValue) { + continue; + } + + if (cellMv < lowestPackCellMv) { + lowestPackCellMv = cellMv; + } + } + } + + for (std::size_t moduleIndex = 0; moduleIndex < kModuleCount; ++moduleIndex) { + ModuleReadings& module = pollData_.modules[moduleIndex]; + module.balanceMask = balanceMaskForModule(module, module.balanceMask, lowestPackCellMv); } for (std::size_t moduleIndex = 0; moduleIndex < kModuleCount; ++moduleIndex) { @@ -311,7 +334,7 @@ void ReadBMS::logModuleSiliconIds(const LogSnapshot& snapshot, Stream& stream) { stream.println(); } -uint16_t ReadBMS::balanceMaskForModule(const ModuleReadings& module, uint16_t currentMask) { +uint16_t ReadBMS::balanceMaskForModule(const ModuleReadings& module, uint16_t currentMask, uint16_t lowestPackCellMv) { if (!module.connected || !module.cellDataValid) { return 0; } @@ -319,7 +342,11 @@ uint16_t ReadBMS::balanceMaskForModule(const ModuleReadings& module, uint16_t cu return 0; } - uint16_t minCellMv = UINT16_MAX; + // grab the min cell voltage for the module, this is the balance target + // thus if the pack min cell voltage is inserted here instead, then each + // module will balance globally to each other + #ifdef BALANCE_MODULES_INDEPENDENTLY + uint16_t targetCellMv = UINT16_MAX; for (uint16_t cellMv : module.cellVoltages) { if (cellMv == adbms6830::BMSInterface::kInvalidCellValue) { return 0; @@ -335,11 +362,19 @@ uint16_t ReadBMS::balanceMaskForModule(const ModuleReadings& module, uint16_t cu if (minCellMv == UINT16_MAX) { return 0; } + #else + // Balance modules to the global min cell voltage + uint16_t targetCellMv = lowestPackCellMv; + if (targetCellMv == UINT16_MAX) { + return 0; + } + #endif uint16_t desiredMask = 0; for (std::size_t cellIndex = 0; cellIndex < module.cellVoltages.size(); ++cellIndex) { const uint16_t cellMv = module.cellVoltages[cellIndex]; - const uint16_t deltaMv = static_cast(cellMv - minCellMv); + // calculate delta voltage from the target cell voltage pulled previously + const uint16_t deltaMv = static_cast(cellMv - targetCellMv); const uint16_t cellBit = static_cast(1u << cellIndex); const bool currentlyBalancing = (currentMask & cellBit) != 0u; if (currentlyBalancing) { @@ -691,3 +726,45 @@ void ReadBMS::copyModuleReadings(ModuleReadings& destination, destination.cellVoltages = source.cellVoltages; destination.thermistorTempsC = source.thermistorTempsC; } + +// Update State of charge struct data +ReadBMS::StateOfCharge ReadBMS::pollSOC() { + uint16_t lowestCellMv = UINT16_MAX; + uint16_t highestCellMv = 0; + float minStateOfCharge = 0.0f; + float maxStateOfCharge = 0.0f; + + // get lastest module readings + updatePollData(); + + for (const ReadBMS::ModuleReadings &module : pollData_.modules) + { + if (!module.connected || !module.cellDataValid) { + continue; + } + + for (uint16_t cellMv : module.cellVoltages) { + if (cellMv == adbms6830::BMSInterface::kInvalidCellValue) { + continue; + } + + if (cellMv < lowestCellMv) { + lowestCellMv = cellMv; + } + + if (cellMv > highestCellMv) { + highestCellMv = cellMv; + } + } + } + + minStateOfCharge = lookUpSOC(lowestCellMv); + maxStateOfCharge = lookUpSOC(highestCellMv); + + StateOfCharge soc{}; + soc.minSOC = minStateOfCharge; + soc.maxSOC = maxStateOfCharge; + soc.minCellMv = lowestCellMv; + soc.maxCellMv = highestCellMv; + return soc; +} diff --git a/src/BMSControl.h b/src/BMSControl.h index 9c9122d..325b2e7 100644 --- a/src/BMSControl.h +++ b/src/BMSControl.h @@ -32,12 +32,21 @@ class ReadBMS { std::array moduleSiliconIds{}; }; + // To be used for charging logic and CAN msg data sent to the dashboard + struct StateOfCharge { + float minSOC = 0.0f; + float maxSOC = 0.0f; + uint16_t minCellMv = 0; + uint16_t maxCellMv = 0; + }; + ReadBMS(); void begin(); void pollBMS(); void updateBalancing(bool enabled); const PollData& data() const; + StateOfCharge pollSOC(); // to pull SOC data for charging LogSnapshot captureLogSnapshot() const; static void logBalancingState(const LogSnapshot& snapshot, Stream& stream); static void logConnectedModules(const LogSnapshot& snapshot, Stream& stream); @@ -82,7 +91,7 @@ class ReadBMS { static AggregateStats cellStatsForModule(const ModuleReadings& module); static AggregateStats thermistorStatsForModule(const ModuleReadings& module); static bool boardThermistorFaulted(const ModuleReadings& module); - static uint16_t balanceMaskForModule(const ModuleReadings& module, uint16_t currentMask); + static uint16_t balanceMaskForModule(const ModuleReadings &module, uint16_t currentMask, uint16_t lowestPackCellMv); static void printSiliconId(Stream& stream, const adbms6830::BMSInterface::SiliconIdReadback& siliconId); void applyBalanceMask(adbms6830::BMSInterface& bmsInterface, std::array& appliedMasks, @@ -115,6 +124,8 @@ class ReadBMS { const adbms6830::BMSInterface::ModuleData& source, bool connected) const; + float lookUpSOC(uint16_t cellMv); + static const SPISettings kBmsSpiSettings; adbms6830::ADBMS6830Driver mainBmsDriver_; diff --git a/src/JboxIO.cpp b/src/JboxIO.cpp index 3b5f0e8..8ffd42c 100644 --- a/src/JboxIO.cpp +++ b/src/JboxIO.cpp @@ -8,14 +8,6 @@ void JboxIO::init() { pinMode(CHARGE_ENABLE_SENSE, INPUT); } -bool JboxIO::readDriveEnable() const { - return digitalRead(DRIVE_ENABLE_SENSE) == HIGH; -} - -bool JboxIO::readChargeEnable() const { - return digitalRead(CHARGE_ENABLE_SENSE) == HIGH; -} - void JboxIO::setStatus(StatusMode mode) const { digitalWrite(BMS_STATUS_OUTPUT, mode == StatusMode::GOOD ? HIGH : LOW); } diff --git a/src/MCP2517Can.h b/src/MCP2517Can.h index e8ad3a9..98e39d5 100644 --- a/src/MCP2517Can.h +++ b/src/MCP2517Can.h @@ -24,6 +24,26 @@ class MCP2517Can { Osc40MHzDiv2, }; + enum CanMsgId : uint32_t { + MotorControlCommand = 0x0C0, // Motor control command, BMS will switch to drive ready mode when this msg is recieved + + BmsStatus = 0x070, // BMS status message + StateOfCharge = 0x072, // BMS state of charge + + ChargerControl = 0x1806E5F4, // ELCON CAN 3865 charger control message id + ChargerStatus = 0x18FF50E5, // ELCON CAN 3865 charger status broadcast message + }; + + enum ChargerControl : bool { + ChargerStart = 0, + ChargerClose = 1, + }; + + enum ChargingMode : bool { + ChargingMode = 0, + HeatingMode = 1, + }; + struct Message { uint32_t id = 0; bool extended = false; diff --git a/src/main.cpp b/src/main.cpp index 50eb1b2..4b11de2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,7 @@ bool core1_separate_stack = true; -#define DANGEROUS_MODE true +// #define BALANCE_MODULES_INDEPENDENTLY namespace { constexpr MCP2517Can::Oscillator kCanOscillator = MCP2517Can::Oscillator::Osc40MHz; @@ -33,11 +33,28 @@ namespace { uint32_t lastPollMs = 0; uint32_t lastLogMs = 0; uint32_t lastCanStatusMs = 0; + uint32_t lastCanChargerMS = 0; + uint32_t lastChargerTimeoutMs = 0; + uint32_t lastChargerControlMessageMs = 0; + uint32_t lastChargerStatusUpdateMs = 0; + uint32_t lastChargerLoggingMs = 0; uint8_t logCycleCount = 0; mutex_t gBmsDataMutex; volatile bool gBmsDataReady = false; bool gCan0Ready = false; + // charging states + enum ChargingState : uint8_t { + DISABLED = 0, // In drive mode OR Charger CAN status msg timeout + IDLE, // Charge enable pin detected + READY, // Charger CAN status msg recieved + CHARGING, // Safe to charge + COMPLETE, // charging completed + FAULT // Fault detected send msg to charger to STOP charging + }; + + ChargingState chargingState = ChargingState::DISABLED; + uint8_t encodeAggregateStatus(StatusMode mode) { switch (mode) { case StatusMode::GOOD: @@ -166,7 +183,7 @@ namespace { MCP2517Can::Message buildCanStatusMessage(const SystemStatuses& statuses, const ReadBMS::PollData& pollData) { MCP2517Can::Message message; - message.id = constants::kCanStatusMessageId; + message.id = MCP2517Can::CanMsgId::BmsStatus; message.length = constants::kCanStatusPayloadLength; writeBits(message.data, 0, 2, encodeAggregateStatus(statuses.BMS)); @@ -192,6 +209,67 @@ namespace { return message; } + // State of Charge CAN message + MCP2517Can::Message buildCanSOCMessage(const SystemStatuses& statuses, ReadBMS::StateOfCharge& soc) { + MCP2517Can::Message message; + message.id = MCP2517Can::CanMsgId::StateOfCharge; + message.length = 6; + // if in charging mode send the SOC of the maxCellMv else send the SOC of the minCellMv + if (chargingState == ChargingState::CHARGING) { + message.data[0] = soc.maxSOC; + } else { + message.data[0] = soc.minSOC; + } + + message.data[4] = soc.minCellMv; + + return message; + } + + // BMS CAN msg for Elcon charger communication + MCP2517Can::Message buildCanChargerControlMessage(uint16_t maxChargingVoltageV, uint16_t maxChargingCurrentA, bool chargerControl, bool chargerMode) { + MCP2517Can::Message message; + message.id = MCP2517Can::CanMsgId::ChargerControl; + message.length = 8; + message.extended = true; + + // convert voltage to decivolts + uint16_t rawMaxChargingVoltageV = static_cast(maxChargingVoltageV * 10); + // convert amperage to deciamps + uint16_t rawMaxChargingCurrentA = static_cast(maxChargingCurrentA * 10); + + // Max allowable charging terminal + message.data[0] = rawMaxChargingVoltageV >> 8; // high byte + message.data[1] = rawMaxChargingVoltageV & 0xFF; // low byte + + // Max allowable charging current + message.data[2] = rawMaxChargingCurrentA >> 8; // high byte + message.data[3] = rawMaxChargingCurrentA & 0xFF; // low byte + + // Control + message.data[4] = chargerControl; + + return message; + } + + // print charger status CAN msg for debugging + void printChargerStatus(MCP2517Can::Message statusMsg) { + uint16_t outputVoltage = static_cast(statusMsg.data[0] << 8) | statusMsg.data[1]; + uint16_t outputCurrent = static_cast(statusMsg.data[2] << 8) | statusMsg.data[3]; + uint8_t statusFlags = statusMsg.data[4]; + + Serial.println(); + Serial.print("Charger output voltage: "); + Serial.print(static_cast(outputVoltage) / 10.0); + Serial.println(" V"); + Serial.print("Charger output current: "); + Serial.print(static_cast(outputCurrent) / 10.0); + Serial.println(" A"); + Serial.print("Charger status flags of 0x"); + Serial.print(statusFlags, 16); + Serial.println(); + } + void configureCan0Spi() { SPI.setRX(CAN_SPI_MISO); SPI.setSCK(CAN_SPI_SCLK); @@ -202,7 +280,7 @@ namespace { pinMode(CAN0_INT, INPUT_PULLUP); digitalWrite(CAN0_CS, HIGH); } -} // namespace +} // namespace void setup() { mutex_init(&gBmsDataMutex); @@ -232,8 +310,6 @@ void loop() { lastPollMs = now; SystemStatuses statusesForOutput{}; - // TODO read drive and charge enable pin - Not sure how this effects status yet - mutex_enter_blocking(&gBmsDataMutex); // Poll the slave boards, and get the latest readings @@ -257,13 +333,13 @@ void loop() { ledControl.update(statusesForOutput, balancingOn); } - } void setup1() { // Serial output is used for periodic module telemetry Serial.begin(115200); + // set up CAN configureCan0Spi(); gCan0Ready = can0.begin(); @@ -271,6 +347,7 @@ void setup1() { Serial.print(gCan0Ready ? "ok" : "failed"); Serial.print(" error=0x"); Serial.println(can0.lastError(), HEX); + } void loop1() { @@ -283,6 +360,140 @@ void loop1() { can0.poll(); } + // check for incoming CAN messages + MCP2517Can::Message rmsg; + if (can0.receive(rmsg)) { + switch (static_cast(rmsg.id)) { + case MCP2517Can::CanMsgId::ChargerStatus: + Serial.println("Charger msg recieved"); + // reset charger status CAN msg timeout + lastChargerTimeoutMs = now; + + if(rmsg.data[4] != 0) { + // According to the elcon spec, if any of the flags are set it + // means something has gone amiss. We don't care about the + // specifics, so we just call it a fault. + // chargingState = ChargingState::FAULT; + Serial.println("Charger flag fault: 0x"); + Serial.print(rmsg.data[4], 16); + } else if (chargingState == ChargingState::DISABLED) { + // update charger mode + chargingState = ChargingState::READY; + } + + printChargerStatus(rmsg); + break; + case MCP2517Can::CanMsgId::MotorControlCommand: + Serial.println("Motor command msg recieved"); + // BMS is in drive mode so disable charging + chargingState = ChargingState::DISABLED; + break; + default: + break; + } + } + + // if charger status CAN msg was not recieved for 2 seconds, turn off charging mode + if (now - lastChargerTimeoutMs >= constants::kCanChargerTimeOutMs) { + chargingState = ChargingState::DISABLED; + } + + // charging logic states + if (now - lastChargerStatusUpdateMs >= constants::kChargerStatusUpdateIntervalMs) { + lastChargerStatusUpdateMs = now; + ReadBMS::StateOfCharge soc{}; + SystemStatuses statusesSnapshot{}; + ReadBMS::PollData pollSnapshot{}; + + // get the lastest SOC and BMS status readings + mutex_enter_blocking(&gBmsDataMutex); + statusesSnapshot = gSystemStatuses; + soc = readBms.pollSOC(); + pollSnapshot = readBms.data(); + mutex_exit(&gBmsDataMutex); + + switch (static_cast(chargingState)) { + case ChargingState::READY: + // check if safe to charge by checking voltages and temps and we should test overall BMS status too for redundancy + if ((soc.maxCellMv < constants::kCellVoltageGoodMaxMv) && + (statusesSnapshot.temp == StatusMode::GOOD) && + (statusesSnapshot.BMS == StatusMode::GOOD)) + { + chargingState = ChargingState::CHARGING; + } + break; + case ChargingState::CHARGING: + // check if charging is complete + if (soc.maxCellMv >= constants::kCellVoltageGoodMaxMv) { + chargingState = ChargingState::COMPLETE; + } + // check temperature + if (encodeHighestTempC(pollSnapshot) >= constants::kTempChargingFaultC) { + chargingState = ChargingState::FAULT; + Serial.println("Charger temp fault 476"); + } + // for safety, redundant check of BMS status + if (statusesSnapshot.BMS != StatusMode::GOOD) { + chargingState = ChargingState::FAULT; + Serial.println("Charger fault from BMS fault 481"); + } + break; + case ChargingState::FAULT: + { + // send msg to charger to STOP charging + const MCP2517Can::Message fault_msg = buildCanChargerControlMessage( + constants::kVoltageChargerMaxPackV, + 0, + MCP2517Can::ChargerControl::ChargerClose, + MCP2517Can::ChargingMode::ChargingMode); + + if (can0.send(fault_msg)) { + Serial.println("Charger shutdown msg sent"); + } + } + break; + default: + break; + } + } + + // send charging CAN msg at a rate of once per second per charger datasheet + if (gCan0Ready && + (now - lastChargerControlMessageMs >= constants::kCanChargerControlIntervalMs) && + (chargingState == ChargingState::CHARGING || chargingState == ChargingState::COMPLETE)) { + lastChargerControlMessageMs = now; + + uint16_t targetAmperage = 0; + bool chargerControl = MCP2517Can::ChargerControl::ChargerClose; + // should always be in charging mode + bool chargerMode = MCP2517Can::ChargingMode::ChargingMode; + + switch (static_cast(chargingState)) { + case ChargingState::CHARGING: + targetAmperage = constants::kStartChargeA; + chargerControl = MCP2517Can::ChargerControl::ChargerStart; + break; + case ChargingState::COMPLETE: + // send msg to charger to STOP charging + targetAmperage = 0; + chargerControl = MCP2517Can::ChargerControl::ChargerClose; + break; + default: + break; + } + + MCP2517Can::Message msg = buildCanChargerControlMessage( + constants::kVoltageChargerMaxPackV, + targetAmperage, + chargerControl, + chargerMode); + + if (can0.send(msg)) { + Serial.println("Charger control message sent success"); + } + } + + // Update balancing through serial GUI interface while (Serial.available() > 0) { const char ch = static_cast(Serial.read()); if (ch == '\r') { @@ -317,6 +528,7 @@ void loop1() { bmsSnapshot = readBms.captureLogSnapshot(); mutex_exit(&gBmsDataMutex); + Serial.println(); Serial.print("status BMS: "); Serial.println(statusModeName(statusesSnapshot.BMS)); Serial.print("status board: "); @@ -325,6 +537,22 @@ void loop1() { Serial.println(statusModeName(statusesSnapshot.voltage)); Serial.print("status temp: "); Serial.println(statusModeName(statusesSnapshot.temp)); + Serial.print("Charging State: "); + if (chargingState == ChargingState::CHARGING) { + Serial.println("CHARGING"); + } else if (chargingState == ChargingState::DISABLED) { + Serial.println("CHARGING DISABLED"); + } else if (chargingState == ChargingState::IDLE) { + Serial.println("CHARGING IDLE"); + } else if (chargingState == ChargingState::READY) { + Serial.println("CHARGING READY"); + } else if (chargingState == ChargingState::COMPLETE) { + Serial.println("CHARGING COMPLETE"); + } else { + Serial.println("CHARGING FAULT"); + } + Serial.println(); + ReadBMS::logBalancingState(bmsSnapshot, Serial); ReadBMS::logConnectedModules(bmsSnapshot, Serial); @@ -334,19 +562,30 @@ void loop1() { } } + // send BMS statues and basic information through CAN if (gCan0Ready && (now - lastCanStatusMs >= constants::kCanStatusIntervalMs)) { lastCanStatusMs = now; SystemStatuses statusesSnapshot{}; ReadBMS::PollData pollSnapshot{}; + ReadBMS::StateOfCharge soc{}; mutex_enter_blocking(&gBmsDataMutex); statusesSnapshot = gSystemStatuses; pollSnapshot = readBms.data(); + soc = readBms.pollSOC(); mutex_exit(&gBmsDataMutex); const MCP2517Can::Message statusMessage = buildCanStatusMessage(statusesSnapshot, pollSnapshot); - if (!can0.send(statusMessage)) { - Serial.println("CAN0 status send failed"); + if (can0.send(statusMessage)) { + Serial.println("CAN0 status message sent"); + Serial.println(); } + + const MCP2517Can::Message socMessage = buildCanSOCMessage(statusesSnapshot, soc); + if (can0.send(socMessage)) { + Serial.println("CAN0 SOC message sent"); + Serial.println(); + } + } -} +} \ No newline at end of file