From 7b0a9afd09d00d4379e2c95f42286029bf8a7555 Mon Sep 17 00:00:00 2001 From: levi-potter Date: Thu, 4 Jun 2026 23:03:42 -0600 Subject: [PATCH 1/5] charging logic --- include/CONSTANTS.h | 18 +++++- include/SOCLookUpTable.h | 38 ++++++++++++ platformio.ini | 13 ++-- src/BMSControl.cpp | 74 ++++++++++++++++++++++ src/BMSControl.h | 11 ++++ src/main.cpp | 131 ++++++++++++++++++++++++++++++++++++++- 6 files changed, 274 insertions(+), 11 deletions(-) create mode 100644 include/SOCLookUpTable.h diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index c560fe8..fa0eef2 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -24,8 +24,14 @@ namespace constants { constexpr uint32_t kModuleScanIntervalMs = 2000; constexpr uint32_t kCanBitRate = 250000; - constexpr uint32_t kCanStatusIntervalMs = 500; + 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 = 1000; // Rate at which the charger needs a CAN control message constexpr uint32_t kCanStatusMessageId = 0x070; + constexpr uint32_t kCanSOCMessageId = 0x072; + constexpr uint32_t kCanChargerControlMessageId = 0x1806E5F4; // ELCON CAN 3865 charger operating message id + constexpr uint32_t kCanElconChargerStatusMessageId = 0x18FF50E5; // ELCON CAN 3865 charger status broadcast message constexpr uint8_t kCanStatusPayloadLength = 8; constexpr uint8_t kConnectDebounce = 2; @@ -50,4 +56,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/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..6f4cc73 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,76 @@ 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; + // convert Mv to V + soc.totalPackVoltageMv = static_cast(totalCellMv * 0.001); + return soc; +} diff --git a/src/BMSControl.h b/src/BMSControl.h index 9c9122d..62065ab 100644 --- a/src/BMSControl.h +++ b/src/BMSControl.h @@ -32,12 +32,21 @@ class ReadBMS { std::array moduleSiliconIds{}; }; + struct StateOfCharge { + float minSOC = 0.0f; + float maxSOC = 0.0f; + uint16_t minCellMv = 0; + uint16_t maxCellMv = 0; + uint16_t totalPackVoltageMv = 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/main.cpp b/src/main.cpp index 50eb1b2..2daf312 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,7 @@ bool core1_separate_stack = true; +// TODO this needs to be taken out #define DANGEROUS_MODE true namespace { @@ -33,10 +34,14 @@ 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; uint8_t logCycleCount = 0; mutex_t gBmsDataMutex; volatile bool gBmsDataReady = false; bool gCan0Ready = false; + bool chargingMode = false; uint8_t encodeAggregateStatus(StatusMode mode) { switch (mode) { @@ -192,6 +197,45 @@ namespace { return message; } + // State of Charge CAN message + MCP2517Can::Message buildCanSOCMessage(const SystemStatuses& statuses, ReadBMS::StateOfCharge& soc) { + MCP2517Can::Message message; + message.id = constants::kCanSOCMessageId; + message.length = 6; + // if in charging mode send the SOC of the maxCellMv else send the SOC of the minCellMv + if (chargingMode) { + writeBits(message.data, 0, 32, soc.maxSOC); + } else { + writeBits(message.data, 0, 32, soc.minSOC); + } + + writeBits(message.data, 32, 48, soc.minCellMv); + + return message; + } + + // BMS CAN msg for Elcon charger communication + MCP2517Can::Message buildCanChargerControlMessage(float maxChargingVoltageV, float maxChargingCurrentA, bool chargerControl, bool chargerStatus) { + MCP2517Can::Message message; + message.id = constants::kCanChargerControlMessageId; + message.length = 6; + message.extended = true; + + uint16_t rawMaxChargingVoltageV = static_cast(maxChargingVoltageV * 10); + uint16_t rawMaxChargingCurrentA = static_cast(maxChargingCurrentA * 10); + + writeBits(message.data, 0, 8, ((rawMaxChargingVoltageV >> 8) & 0xFF)); + writeBits(message.data, 8, 16, (rawMaxChargingVoltageV & 0xFF)); + + writeBits(message.data, 16, 24, ((rawMaxChargingCurrentA >> 8) & 0xFF)); + writeBits(message.data, 24, 32, (rawMaxChargingCurrentA & 0xFF)); + + writeBits(message.data, 32, 1, chargerControl); + writeBits(message.data, 40, 1, chargerStatus); + + return message; + } + void configureCan0Spi() { SPI.setRX(CAN_SPI_MISO); SPI.setSCK(CAN_SPI_SCLK); @@ -220,6 +264,10 @@ void setup() { ledControl.update(gSystemStatuses, balancingOn); gBmsDataReady = true; + + // set up CAN + configureCan0Spi(); + gCan0Ready = can0.begin(); } void loop() { @@ -257,16 +305,65 @@ void loop() { ledControl.update(statusesForOutput, balancingOn); } + MCP2517Can::Message rmsg; + + // update charging mode + if ((now - lastCanChargerMS >= constants::kCanChargerIntervalMs) && + gCan0Ready && + can0.receive(rmsg) && + rmsg.id == constants::kCanElconChargerStatusMessageId && + !chargingMode) + { + // check to see if CAN message received is from Elcon charger, and if so update charging mode + lastCanChargerMS = now; + chargingMode = true; + } + + if ((now - lastChargerTimeoutMs >= constants::kCanChargerTimeOutMs) && + !can0.receive(rmsg) && + chargingMode) + { + // if charger CAN msg was not sent for 5 seconds, turn off charger mode + lastChargerTimeoutMs = now; + chargingMode = false; + } + if ((now - lastChargerTimeoutMs >= constants::kCanChargerTimeOutMs) && + can0.receive(rmsg) && + chargingMode && + rmsg.id == constants::kCanElconChargerStatusMessageId) + { + // reset timeout timer + lastChargerTimeoutMs = now; + } + + // Send CAN charging control msg to Elcon charger if in chargingMode + if (chargingMode && (now - lastChargerControlMessageMs >= constants::kCanChargerControlIntervalMs)) + { + lastChargerControlMessageMs = now; + ReadBMS::StateOfCharge soc{}; + + // get the lastest SOC readings + mutex_enter_blocking(&gBmsDataMutex); + soc = readBms.pollSOC(); + mutex_exit(&gBmsDataMutex); + + if (soc.maxCellMv < constants::kCellVoltageGoodMaxMv) + { + // send CAN charging msg + const MCP2517Can::Message chargerControlMessage = buildCanChargerControlMessage(constants::kVoltageChargerMaxPackV, constants::kStartChargeA, 0, 0); + if (!can0.send(chargerControlMessage)) + { + Serial.println("CAN0 Charger Control message send failed"); + } + } + } } void setup1() { // Serial output is used for periodic module telemetry Serial.begin(115200); - configureCan0Spi(); - gCan0Ready = can0.begin(); - Serial.print("CAN0 init "); Serial.print(gCan0Ready ? "ok" : "failed"); Serial.print(" error=0x"); @@ -325,6 +422,17 @@ void loop1() { Serial.println(statusModeName(statusesSnapshot.voltage)); Serial.print("status temp: "); Serial.println(statusModeName(statusesSnapshot.temp)); + // charging status + Serial.print("Charge Mode: "); + if (chargingMode) + { + Serial.println("TRUE"); + } + else + { + Serial.println("FALSE"); + } + ReadBMS::logBalancingState(bmsSnapshot, Serial); ReadBMS::logConnectedModules(bmsSnapshot, Serial); @@ -338,15 +446,32 @@ 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"); } + + const MCP2517Can::Message socMessage = buildCanSOCMessage(statusesSnapshot, soc); + if (!can0.send(socMessage)) { + Serial.println("CAN0 SOC message send failed"); + } + + // for debuging charger CAN status + MCP2517Can::Message rmsg; + if (can0.receive(rmsg)) + { + if (rmsg.id == constants::kCanElconChargerStatusMessageId) + { + Serial.println("Charger CAN status message received"); + } + } } } From 592d9bdbec0589dfe042ae0bef6603130310238b Mon Sep 17 00:00:00 2001 From: levi-potter Date: Fri, 5 Jun 2026 20:37:08 -0600 Subject: [PATCH 2/5] dangerous mode removed --- include/SystemStatus.h | 8 -------- src/main.cpp | 3 --- 2 files changed, 11 deletions(-) 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/src/main.cpp b/src/main.cpp index 2daf312..b79149d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,9 +14,6 @@ bool core1_separate_stack = true; -// TODO this needs to be taken out -#define DANGEROUS_MODE true - namespace { constexpr MCP2517Can::Oscillator kCanOscillator = MCP2517Can::Oscillator::Osc40MHz; From fa95cc09886b7592329118614d54167639865028 Mon Sep 17 00:00:00 2001 From: levi-potter Date: Fri, 5 Jun 2026 22:28:04 -0600 Subject: [PATCH 3/5] cleaner charging logic --- include/CONSTANTS.h | 4 -- src/MCP2517Can.h | 9 ++++ src/main.cpp | 116 ++++++++++++++++++-------------------------- 3 files changed, 57 insertions(+), 72 deletions(-) diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index fa0eef2..0ea6711 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -28,10 +28,6 @@ namespace constants { 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 kCanStatusMessageId = 0x070; - constexpr uint32_t kCanSOCMessageId = 0x072; - constexpr uint32_t kCanChargerControlMessageId = 0x1806E5F4; // ELCON CAN 3865 charger operating message id - constexpr uint32_t kCanElconChargerStatusMessageId = 0x18FF50E5; // ELCON CAN 3865 charger status broadcast message constexpr uint8_t kCanStatusPayloadLength = 8; constexpr uint8_t kConnectDebounce = 2; diff --git a/src/MCP2517Can.h b/src/MCP2517Can.h index e8ad3a9..7709cc1 100644 --- a/src/MCP2517Can.h +++ b/src/MCP2517Can.h @@ -24,6 +24,15 @@ class MCP2517Can { Osc40MHzDiv2, }; + enum CanMsgId : uint32_t + { + 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 + }; + struct Message { uint32_t id = 0; bool extended = false; diff --git a/src/main.cpp b/src/main.cpp index b79149d..8ba1c3c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -168,7 +168,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)); @@ -197,7 +197,7 @@ namespace { // State of Charge CAN message MCP2517Can::Message buildCanSOCMessage(const SystemStatuses& statuses, ReadBMS::StateOfCharge& soc) { MCP2517Can::Message message; - message.id = constants::kCanSOCMessageId; + 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 (chargingMode) { @@ -214,7 +214,7 @@ namespace { // BMS CAN msg for Elcon charger communication MCP2517Can::Message buildCanChargerControlMessage(float maxChargingVoltageV, float maxChargingCurrentA, bool chargerControl, bool chargerStatus) { MCP2517Can::Message message; - message.id = constants::kCanChargerControlMessageId; + message.id = MCP2517Can::CanMsgId::ChargerControl; message.length = 6; message.extended = true; @@ -261,10 +261,6 @@ void setup() { ledControl.update(gSystemStatuses, balancingOn); gBmsDataReady = true; - - // set up CAN - configureCan0Spi(); - gCan0Ready = can0.begin(); } void loop() { @@ -302,41 +298,56 @@ void loop() { ledControl.update(statusesForOutput, balancingOn); } - MCP2517Can::Message rmsg; +} + +void setup1() { + // Serial output is used for periodic module telemetry + Serial.begin(115200); + + // set up CAN + configureCan0Spi(); + gCan0Ready = can0.begin(); - // update charging mode - if ((now - lastCanChargerMS >= constants::kCanChargerIntervalMs) && - gCan0Ready && - can0.receive(rmsg) && - rmsg.id == constants::kCanElconChargerStatusMessageId && - !chargingMode) - { - // check to see if CAN message received is from Elcon charger, and if so update charging mode - lastCanChargerMS = now; - chargingMode = true; + Serial.print("CAN0 init "); + Serial.print(gCan0Ready ? "ok" : "failed"); + Serial.print(" error=0x"); + Serial.println(can0.lastError(), HEX); + +} + +void loop1() { + const uint32_t now = millis(); + if (!gBmsDataReady) { + return; } - if ((now - lastChargerTimeoutMs >= constants::kCanChargerTimeOutMs) && - !can0.receive(rmsg) && - chargingMode) - { - // if charger CAN msg was not sent for 5 seconds, turn off charger mode - lastChargerTimeoutMs = now; - chargingMode = false; + if (gCan0Ready) { + 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; + // update charger mode + if (!chargingMode) {chargingMode = true;} + break; + default: + break; + } } - if ((now - lastChargerTimeoutMs >= constants::kCanChargerTimeOutMs) && - can0.receive(rmsg) && - chargingMode && - rmsg.id == constants::kCanElconChargerStatusMessageId) - { - // reset timeout timer + // if charger status CAN msg was not recieved for 2 seconds, turn off charging mode + if ((now - lastChargerTimeoutMs >= constants::kCanChargerTimeOutMs) && chargingMode) { lastChargerTimeoutMs = now; + chargingMode = false; } // Send CAN charging control msg to Elcon charger if in chargingMode - if (chargingMode && (now - lastChargerControlMessageMs >= constants::kCanChargerControlIntervalMs)) - { + if (chargingMode && (now - lastChargerControlMessageMs >= constants::kCanChargerControlIntervalMs)) { lastChargerControlMessageMs = now; ReadBMS::StateOfCharge soc{}; @@ -345,36 +356,13 @@ void loop() { soc = readBms.pollSOC(); mutex_exit(&gBmsDataMutex); - if (soc.maxCellMv < constants::kCellVoltageGoodMaxMv) - { + if (soc.maxCellMv < constants::kCellVoltageGoodMaxMv) { // send CAN charging msg const MCP2517Can::Message chargerControlMessage = buildCanChargerControlMessage(constants::kVoltageChargerMaxPackV, constants::kStartChargeA, 0, 0); - if (!can0.send(chargerControlMessage)) - { + if (!can0.send(chargerControlMessage)) { Serial.println("CAN0 Charger Control message send failed"); } - } - } -} - -void setup1() { - // Serial output is used for periodic module telemetry - Serial.begin(115200); - - Serial.print("CAN0 init "); - Serial.print(gCan0Ready ? "ok" : "failed"); - Serial.print(" error=0x"); - Serial.println(can0.lastError(), HEX); -} - -void loop1() { - const uint32_t now = millis(); - if (!gBmsDataReady) { - return; - } - - if (gCan0Ready) { - can0.poll(); + } } while (Serial.available() > 0) { @@ -461,14 +449,6 @@ void loop1() { Serial.println("CAN0 SOC message send failed"); } - // for debuging charger CAN status - MCP2517Can::Message rmsg; - if (can0.receive(rmsg)) - { - if (rmsg.id == constants::kCanElconChargerStatusMessageId) - { - Serial.println("Charger CAN status message received"); - } - } } + } From d20b5d9fd07925467d73bd200abae57255dbe3ac Mon Sep 17 00:00:00 2001 From: levi-potter Date: Sat, 6 Jun 2026 13:34:11 -0600 Subject: [PATCH 4/5] charger states and corrected charger control CAN msg --- include/CONSTANTS.h | 3 +- src/MCP2517Can.h | 15 ++++- src/main.cpp | 134 ++++++++++++++++++++++++++++++++------------ 3 files changed, 112 insertions(+), 40 deletions(-) diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index 0ea6711..25a6b41 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -27,7 +27,8 @@ namespace constants { 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 = 1000; // Rate at which the charger needs a CAN control message + 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; diff --git a/src/MCP2517Can.h b/src/MCP2517Can.h index 7709cc1..98e39d5 100644 --- a/src/MCP2517Can.h +++ b/src/MCP2517Can.h @@ -24,8 +24,9 @@ class MCP2517Can { Osc40MHzDiv2, }; - enum CanMsgId : uint32_t - { + 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 @@ -33,6 +34,16 @@ class MCP2517Can { 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 8ba1c3c..29c57e1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,11 +34,22 @@ namespace { 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; - bool chargingMode = 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) { @@ -200,7 +211,7 @@ namespace { 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 (chargingMode) { + if (chargingState == ChargingState::CHARGING) { writeBits(message.data, 0, 32, soc.maxSOC); } else { writeBits(message.data, 0, 32, soc.minSOC); @@ -212,23 +223,32 @@ namespace { } // BMS CAN msg for Elcon charger communication - MCP2517Can::Message buildCanChargerControlMessage(float maxChargingVoltageV, float maxChargingCurrentA, bool chargerControl, bool chargerStatus) { + MCP2517Can::Message buildCanChargerControlMessage(float maxChargingVoltageV, float maxChargingCurrentA, bool chargerControl, bool chargerMode) { MCP2517Can::Message message; message.id = MCP2517Can::CanMsgId::ChargerControl; - message.length = 6; + message.length = 8; message.extended = true; - uint16_t rawMaxChargingVoltageV = static_cast(maxChargingVoltageV * 10); - uint16_t rawMaxChargingCurrentA = static_cast(maxChargingCurrentA * 10); + 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 - writeBits(message.data, 0, 8, ((rawMaxChargingVoltageV >> 8) & 0xFF)); - writeBits(message.data, 8, 16, (rawMaxChargingVoltageV & 0xFF)); + // Control + message.data[4] = chargerControl ? 0x00 : 0x01; - writeBits(message.data, 16, 24, ((rawMaxChargingCurrentA >> 8) & 0xFF)); - writeBits(message.data, 24, 32, (rawMaxChargingCurrentA & 0xFF)); + // Working status control + message.data[5] = chargerMode ? 0x00 : 0x01; - writeBits(message.data, 32, 1, chargerControl); - writeBits(message.data, 40, 1, chargerStatus); + // reserved + message.data[6] = 0x00; + message.data[7] = 0x00; return message; } @@ -333,7 +353,13 @@ void loop1() { // reset charger status CAN msg timeout lastChargerTimeoutMs = now; // update charger mode - if (!chargingMode) {chargingMode = true;} + if (chargingState == ChargingState::DISABLED) { + chargingState = ChargingState::READY; + } + break; + case MCP2517Can::CanMsgId::MotorControlCommand: + // BMS is in drive mode so disable charging + chargingState = ChargingState::DISABLED; break; default: break; @@ -341,28 +367,72 @@ void loop1() { } // if charger status CAN msg was not recieved for 2 seconds, turn off charging mode - if ((now - lastChargerTimeoutMs >= constants::kCanChargerTimeOutMs) && chargingMode) { - lastChargerTimeoutMs = now; - chargingMode = false; + if (now - lastChargerTimeoutMs >= constants::kCanChargerTimeOutMs) { + chargingState = ChargingState::DISABLED; } - // Send CAN charging control msg to Elcon charger if in chargingMode - if (chargingMode && (now - lastChargerControlMessageMs >= constants::kCanChargerControlIntervalMs)) { - lastChargerControlMessageMs = now; + // 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); - if (soc.maxCellMv < constants::kCellVoltageGoodMaxMv) { - // send CAN charging msg - const MCP2517Can::Message chargerControlMessage = buildCanChargerControlMessage(constants::kVoltageChargerMaxPackV, constants::kStartChargeA, 0, 0); - if (!can0.send(chargerControlMessage)) { - Serial.println("CAN0 Charger Control message send failed"); - } - } + 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; + bool chargerMode = MCP2517Can::ChargingMode::ChargingMode; // should always be in charging mode + + 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); + + can0.send(msg); } while (Serial.available() > 0) { @@ -407,16 +477,6 @@ void loop1() { Serial.println(statusModeName(statusesSnapshot.voltage)); Serial.print("status temp: "); Serial.println(statusModeName(statusesSnapshot.temp)); - // charging status - Serial.print("Charge Mode: "); - if (chargingMode) - { - Serial.println("TRUE"); - } - else - { - Serial.println("FALSE"); - } ReadBMS::logBalancingState(bmsSnapshot, Serial); @@ -441,7 +501,7 @@ void loop1() { 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); From 2b73cf18ffed91da5e9dde849c1e8d5e8895fff7 Mon Sep 17 00:00:00 2001 From: levi-potter Date: Sat, 6 Jun 2026 15:52:21 -0600 Subject: [PATCH 5/5] state of charge msg corrected and charger CAN msg feedback established --- src/BMSControl.cpp | 2 -- src/BMSControl.h | 2 +- src/main.cpp | 24 ++++++++++++++++-------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/BMSControl.cpp b/src/BMSControl.cpp index 6f4cc73..3df8799 100644 --- a/src/BMSControl.cpp +++ b/src/BMSControl.cpp @@ -761,7 +761,5 @@ ReadBMS::StateOfCharge ReadBMS::pollSOC() { soc.maxSOC = maxStateOfCharge; soc.minCellMv = lowestCellMv; soc.maxCellMv = highestCellMv; - // convert Mv to V - soc.totalPackVoltageMv = static_cast(totalCellMv * 0.001); return soc; } diff --git a/src/BMSControl.h b/src/BMSControl.h index 62065ab..f6422d8 100644 --- a/src/BMSControl.h +++ b/src/BMSControl.h @@ -32,12 +32,12 @@ 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; - uint16_t totalPackVoltageMv = 0; }; ReadBMS(); diff --git a/src/main.cpp b/src/main.cpp index 29c57e1..ca6ff02 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -212,12 +212,12 @@ namespace { message.length = 6; // if in charging mode send the SOC of the maxCellMv else send the SOC of the minCellMv if (chargingState == ChargingState::CHARGING) { - writeBits(message.data, 0, 32, soc.maxSOC); + message.data[0] = soc.maxSOC; } else { - writeBits(message.data, 0, 32, soc.minSOC); + message.data[0] = soc.minSOC; } - writeBits(message.data, 32, 48, soc.minCellMv); + message.data[4] = soc.minCellMv; return message; } @@ -352,8 +352,13 @@ void loop1() { case MCP2517Can::CanMsgId::ChargerStatus: // reset charger status CAN msg timeout lastChargerTimeoutMs = now; - // update charger mode - if (chargingState == ChargingState::DISABLED) { + 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; @@ -411,8 +416,9 @@ void loop1() { float targetAmperage = 0.0f; bool chargerControl = MCP2517Can::ChargerControl::ChargerClose; - bool chargerMode = MCP2517Can::ChargingMode::ChargingMode; // should always be in charging mode - + // should always be in charging mode + bool chargerMode = MCP2517Can::ChargingMode::ChargingMode; + switch (static_cast(chargingState)) { case ChargingState::CHARGING: targetAmperage = constants::kStartChargeA; @@ -432,7 +438,9 @@ void loop1() { chargerControl, chargerMode); - can0.send(msg); + if (can0.send(msg)) { + Serial.println("Charger control message sent success"); + } } while (Serial.available() > 0) {