diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index c560fe8..25a6b41 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -24,8 +24,11 @@ namespace constants { constexpr uint32_t kModuleScanIntervalMs = 2000; constexpr uint32_t kCanBitRate = 250000; - constexpr uint32_t kCanStatusIntervalMs = 500; - constexpr uint32_t kCanStatusMessageId = 0x070; + constexpr uint32_t kCanStatusIntervalMs = 500; // 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 = 500; // Rate at which the charger needs a CAN control message + constexpr uint32_t kChargerStatusUpdateIntervalMs = 250; constexpr uint8_t kCanStatusPayloadLength = 8; constexpr uint8_t kConnectDebounce = 2; @@ -50,4 +53,12 @@ namespace constants { constexpr std::size_t kLedCount = 13; constexpr uint8_t kLedBrightness = 32; -} // namespace constants + + // TODO constants for charging + constexpr float kSocChargingLimit = 1.0f; // SOC percentage charge limit + constexpr float kVoltageChargerMaxPackV = 445.0f; // 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 uint16_t kStartBalancingMv = 3900; // when any cell reaches this value then balancing will start + constexpr float kMaxChargerPowerOutputW = 6550.0f; // ELCON charger specification + constexpr float kStartChargeA = 18.0f; // 1.0C begining charging amperage + +} // namespace constants diff --git a/include/SOCLookUpTable.h b/include/SOCLookUpTable.h new file mode 100644 index 0000000..6b262b0 --- /dev/null +++ b/include/SOCLookUpTable.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +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 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..8ac12d3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -16,9 +16,10 @@ board_build.core = earlephilhower board_build.filesystem_size = 0.5m monitor_speed = 115200 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 + throwtheswitch/Unity@^2.6.1 diff --git a/src/BMSControl.cpp b/src/BMSControl.cpp index ba82e87..3df8799 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); @@ -691,3 +692,74 @@ void ReadBMS::copyModuleReadings(ModuleReadings& destination, destination.cellVoltages = source.cellVoltages; destination.thermistorTempsC = source.thermistorTempsC; } + +// CHARGING +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]; + } + + for (size_t i=0; i < soclookuptable::kNumLookUpPoints - 1; i++) { + if (cellMv >= soclookuptable::kVoltageTable[i] && cellMv <= soclookuptable::kVoltageTable[i+1]) { + // linear interpolation + 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; +} + +ReadBMS::StateOfCharge ReadBMS::pollSOC() { + uint16_t lowestCellMv = UINT16_MAX; + uint16_t highestCellMv = 0; + float minStateOfCharge = 0.0f; + float maxStateOfCharge = 0.0f; + uint32_t totalCellMv = 0; + + // 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; + } + + totalCellMv += 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..f6422d8 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 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); @@ -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/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..ca6ff02 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,8 +14,6 @@ bool core1_separate_stack = true; -#define DANGEROUS_MODE true - namespace { constexpr MCP2517Can::Oscillator kCanOscillator = MCP2517Can::Oscillator::Osc40MHz; @@ -33,11 +31,26 @@ 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; 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 + READY, // Charger CAN status msg recieved + CHARGING, // Safe to charge + COMPLETE, // charging completed + FAULT // Fault detected + }; + + ChargingState chargingState = ChargingState::DISABLED; + uint8_t encodeAggregateStatus(StatusMode mode) { switch (mode) { case StatusMode::GOOD: @@ -166,7 +179,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 +205,54 @@ 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(float maxChargingVoltageV, float maxChargingCurrentA, bool chargerControl, bool chargerMode) { + MCP2517Can::Message message; + message.id = MCP2517Can::CanMsgId::ChargerControl; + message.length = 8; + message.extended = true; + + uint16_t rawMaxChargingVoltageV = static_cast(maxChargingVoltageV * 10.0f); + uint16_t rawMaxChargingCurrentA = static_cast(maxChargingCurrentA * 10.0f); + + // Max allowable charging terminal + message.data[0] = (rawMaxChargingVoltageV >> 8) & 0xFF; // high byte + message.data[1] = rawMaxChargingVoltageV & 0xFF; // low byte + + // Max allowable charging current + message.data[2] = (rawMaxChargingCurrentA >> 8) & 0xFF; // high byte + message.data[3] = rawMaxChargingCurrentA & 0xFF; // low byte + + // Control + message.data[4] = chargerControl ? 0x00 : 0x01; + + // Working status control + message.data[5] = chargerMode ? 0x00 : 0x01; + + // reserved + message.data[6] = 0x00; + message.data[7] = 0x00; + + return message; + } + void configureCan0Spi() { SPI.setRX(CAN_SPI_MISO); SPI.setSCK(CAN_SPI_SCLK); @@ -257,13 +318,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 +332,7 @@ void setup1() { Serial.print(gCan0Ready ? "ok" : "failed"); Serial.print(" error=0x"); Serial.println(can0.lastError(), HEX); + } void loop1() { @@ -283,6 +345,104 @@ 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: + // 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; + } else if (chargingState == ChargingState::DISABLED) { + // update charger mode + chargingState = ChargingState::READY; + } + break; + case MCP2517Can::CanMsgId::MotorControlCommand: + // 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{}; + + // get the lastest SOC readings + mutex_enter_blocking(&gBmsDataMutex); + statusesSnapshot = gSystemStatuses; + soc = readBms.pollSOC(); + mutex_exit(&gBmsDataMutex); + + switch (static_cast(chargingState)) { + case ChargingState::READY: + // check if safe to charge by checking voltages and temps + if ((soc.maxCellMv < constants::kCellVoltageGoodMaxMv) && (statusesSnapshot.temp == StatusMode::GOOD)) { + chargingState = ChargingState::CHARGING; + } + break; + case ChargingState::CHARGING: + // check if charging is complete + if (soc.maxCellMv >= constants::kCellVoltageGoodMaxMv) { + chargingState = ChargingState::COMPLETE; + } + break; + case ChargingState::FAULT: + // Set BMS_STATUS_OUTPUT, pull high if fault + jbox.setStatus(statusesSnapshot.BMS); + break; + default: + break; + } + } + + // send charging CAN msg + if (gCan0Ready && (now - lastChargerControlMessageMs >= constants::kCanChargerControlIntervalMs)) { + lastChargerControlMessageMs = now; + + float targetAmperage = 0.0f; + 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: + targetAmperage = 0.0f; + 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"); + } + } + while (Serial.available() > 0) { const char ch = static_cast(Serial.read()); if (ch == '\r') { @@ -325,6 +485,7 @@ void loop1() { Serial.println(statusModeName(statusesSnapshot.voltage)); Serial.print("status temp: "); Serial.println(statusModeName(statusesSnapshot.temp)); + ReadBMS::logBalancingState(bmsSnapshot, Serial); ReadBMS::logConnectedModules(bmsSnapshot, Serial); @@ -338,15 +499,24 @@ void loop1() { 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"); + Serial.println("CAN0 status message send failed"); + } + + const MCP2517Can::Message socMessage = buildCanSOCMessage(statusesSnapshot, soc); + if (!can0.send(socMessage)) { + Serial.println("CAN0 SOC message send failed"); } + } + }