From 7b0a9afd09d00d4379e2c95f42286029bf8a7555 Mon Sep 17 00:00:00 2001 From: levi-potter Date: Thu, 4 Jun 2026 23:03:42 -0600 Subject: [PATCH 01/18] 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 02/18] 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 03/18] 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 04/18] 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 05/18] 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) { From 30e52836a2e11b1c08454a162386b6ef7dc2861b Mon Sep 17 00:00:00 2001 From: levi-potter Date: Mon, 8 Jun 2026 19:43:30 -0600 Subject: [PATCH 06/18] reorganize SOC lookup function and improved charging logic --- include/CONSTANTS.h | 2 +- include/SOCLookUpTable.h | 30 ++++++++++++++++++++++++++++++ src/BMSControl.cpp | 31 +------------------------------ src/BMSControl.h | 2 +- src/main.cpp | 12 +++++++++--- 5 files changed, 42 insertions(+), 35 deletions(-) diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index 25a6b41..970b2cf 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -27,7 +27,7 @@ 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 = 500; // Rate at which the charger needs a CAN control message + constexpr uint32_t kCanChargerControlIntervalMs = 1000; // Rate at which the charger needs a CAN control message constexpr uint32_t kChargerStatusUpdateIntervalMs = 250; constexpr uint8_t kCanStatusPayloadLength = 8; diff --git a/include/SOCLookUpTable.h b/include/SOCLookUpTable.h index 6b262b0..c1927bf 100644 --- a/include/SOCLookUpTable.h +++ b/include/SOCLookUpTable.h @@ -3,6 +3,10 @@ #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; @@ -36,3 +40,29 @@ namespace soclookuptable { }; } // 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/src/BMSControl.cpp b/src/BMSControl.cpp index 3df8799..1c214d8 100644 --- a/src/BMSControl.cpp +++ b/src/BMSControl.cpp @@ -693,39 +693,12 @@ void ReadBMS::copyModuleReadings(ModuleReadings& destination, 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; -} - +// 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; - uint32_t totalCellMv = 0; // get lastest module readings updatePollData(); @@ -748,8 +721,6 @@ ReadBMS::StateOfCharge ReadBMS::pollSOC() { if (cellMv > highestCellMv) { highestCellMv = cellMv; } - - totalCellMv += cellMv; } } diff --git a/src/BMSControl.h b/src/BMSControl.h index f6422d8..f2d2bb3 100644 --- a/src/BMSControl.h +++ b/src/BMSControl.h @@ -32,7 +32,7 @@ class ReadBMS { std::array moduleSiliconIds{}; }; - // To be used for charging logic and CAN msg sent to the dashboard + // To be used for charging logic and CAN msg data sent to the dashboard struct StateOfCharge { float minSOC = 0.0f; float maxSOC = 0.0f; diff --git a/src/main.cpp b/src/main.cpp index ca6ff02..2efe02f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,6 +14,8 @@ bool core1_separate_stack = true; +// #define COMMUNICATE_WITH_CHARGER + namespace { constexpr MCP2517Can::Oscillator kCanOscillator = MCP2517Can::Oscillator::Osc40MHz; @@ -402,7 +404,7 @@ void loop1() { } break; case ChargingState::FAULT: - // Set BMS_STATUS_OUTPUT, pull high if fault + // Set BMS_STATUS_OUTPUT, pull if fault jbox.setStatus(statusesSnapshot.BMS); break; default: @@ -411,7 +413,11 @@ void loop1() { } // send charging CAN msg - if (gCan0Ready && (now - lastChargerControlMessageMs >= constants::kCanChargerControlIntervalMs)) { + // Only send this msg if charging state is CHARGING or COMPLETE + #ifdef COMMUNICATE_WITH_CHARGER + if (gCan0Ready && + (now - lastChargerControlMessageMs >= constants::kCanChargerControlIntervalMs) && + (chargingState == ChargingState::CHARGING || chargingState == ChargingState::COMPLETE)) { lastChargerControlMessageMs = now; float targetAmperage = 0.0f; @@ -442,6 +448,7 @@ void loop1() { Serial.println("Charger control message sent success"); } } + #endif while (Serial.available() > 0) { const char ch = static_cast(Serial.read()); @@ -518,5 +525,4 @@ void loop1() { } } - } From f0ad30f4cef84859a7ebaa1d5e3a54c14a7e33e4 Mon Sep 17 00:00:00 2001 From: levi-potter Date: Tue, 9 Jun 2026 19:45:28 -0600 Subject: [PATCH 07/18] balance modules on global min cell voltage --- src/BMSControl.cpp | 42 ++++++++++++++++++++++++++++++++++++++---- src/BMSControl.h | 2 +- src/main.cpp | 2 ++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/src/BMSControl.cpp b/src/BMSControl.cpp index 1c214d8..8d851b0 100644 --- a/src/BMSControl.cpp +++ b/src/BMSControl.cpp @@ -78,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) { @@ -312,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; } @@ -320,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; @@ -336,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) { diff --git a/src/BMSControl.h b/src/BMSControl.h index f2d2bb3..325b2e7 100644 --- a/src/BMSControl.h +++ b/src/BMSControl.h @@ -91,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, diff --git a/src/main.cpp b/src/main.cpp index 2efe02f..adc3487 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,8 @@ bool core1_separate_stack = true; // #define COMMUNICATE_WITH_CHARGER +// #define BALANCE_MODULES_INDEPENDENTLY + namespace { constexpr MCP2517Can::Oscillator kCanOscillator = MCP2517Can::Oscillator::Osc40MHz; From 9a5f0ea6dd0a99251da0253861d32875ab902ff2 Mon Sep 17 00:00:00 2001 From: levi-potter Date: Tue, 9 Jun 2026 21:49:10 -0600 Subject: [PATCH 08/18] charger temperature logic --- include/CONSTANTS.h | 9 +++++---- src/main.cpp | 14 +++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index 970b2cf..80aef5a 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -46,9 +46,10 @@ namespace constants { constexpr uint16_t kBalanceMaxCellMv = 5500; constexpr float kTempWarningMinC = 5.0f; - constexpr float kTempGoodMaxC = 50.0f; - constexpr float kTempWarningMaxC = 60.0f; - constexpr float kTempExhaustedMaxC = 70.0f; + constexpr float kTempGoodMaxC = 55.0f; + constexpr uint8_t kTempChargingFaultC = 65; + constexpr float kTempWarningMaxC = 75.0f; // if cells reach 75 C then the BMS should error + constexpr float kTempExhaustedMaxC = 80.0f; // the cells should not go any higher than this per the datasheet constexpr float kBoardThermistorFaultMinC = 70.0f; constexpr std::size_t kLedCount = 13; @@ -59,6 +60,6 @@ namespace constants { 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 + constexpr float kStartChargeA = 9.0f; // 1.0C begining charging amperage } // namespace constants diff --git a/src/main.cpp b/src/main.cpp index adc3487..508cf53 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,7 +14,7 @@ bool core1_separate_stack = true; -// #define COMMUNICATE_WITH_CHARGER +#define COMMUNICATE_WITH_CHARGER // #define BALANCE_MODULES_INDEPENDENTLY @@ -233,7 +233,9 @@ namespace { message.length = 8; message.extended = true; + // convert voltage to decivolts uint16_t rawMaxChargingVoltageV = static_cast(maxChargingVoltageV * 10.0f); + // convert amperage to deciamps uint16_t rawMaxChargingCurrentA = static_cast(maxChargingCurrentA * 10.0f); // Max allowable charging terminal @@ -385,11 +387,13 @@ void loop1() { lastChargerStatusUpdateMs = now; ReadBMS::StateOfCharge soc{}; SystemStatuses statusesSnapshot{}; + ReadBMS::PollData pollSnapshot{}; // get the lastest SOC readings mutex_enter_blocking(&gBmsDataMutex); statusesSnapshot = gSystemStatuses; soc = readBms.pollSOC(); + pollSnapshot = readBms.data(); mutex_exit(&gBmsDataMutex); switch (static_cast(chargingState)) { @@ -404,10 +408,10 @@ void loop1() { if (soc.maxCellMv >= constants::kCellVoltageGoodMaxMv) { chargingState = ChargingState::COMPLETE; } - break; - case ChargingState::FAULT: - // Set BMS_STATUS_OUTPUT, pull if fault - jbox.setStatus(statusesSnapshot.BMS); + // check temperature + if (encodeHighestTempC(pollSnapshot) >= constants::kTempChargingFaultC) { + chargingState = ChargingState::FAULT; + } break; default: break; From 6517b80a37447178ed264ebe1680c15c7e805856 Mon Sep 17 00:00:00 2001 From: levi-potter Date: Tue, 9 Jun 2026 22:30:53 -0600 Subject: [PATCH 09/18] changed CAN data msg send rate --- include/CONSTANTS.h | 2 +- src/main.cpp | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index 80aef5a..0ed7c2e 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -24,7 +24,7 @@ namespace constants { constexpr uint32_t kModuleScanIntervalMs = 2000; constexpr uint32_t kCanBitRate = 250000; - constexpr uint32_t kCanStatusIntervalMs = 500; // Rate at which data CAN status information is sent + 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 diff --git a/src/main.cpp b/src/main.cpp index 508cf53..5b0ce93 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -498,7 +498,13 @@ 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 { + Serial.println("CHARGING DISABLED"); + } + ReadBMS::logBalancingState(bmsSnapshot, Serial); ReadBMS::logConnectedModules(bmsSnapshot, Serial); @@ -521,13 +527,13 @@ void loop1() { mutex_exit(&gBmsDataMutex); const MCP2517Can::Message statusMessage = buildCanStatusMessage(statusesSnapshot, pollSnapshot); - if (!can0.send(statusMessage)) { - Serial.println("CAN0 status message send failed"); + if (can0.send(statusMessage)) { + Serial.println("CAN0 status message sent"); } const MCP2517Can::Message socMessage = buildCanSOCMessage(statusesSnapshot, soc); - if (!can0.send(socMessage)) { - Serial.println("CAN0 SOC message send failed"); + if (can0.send(socMessage)) { + Serial.println("CAN0 SOC message sent"); } } From fa6ceed4a532ed4222ecf4c74102ad40d0b0564f Mon Sep 17 00:00:00 2001 From: levi-potter Date: Wed, 10 Jun 2026 20:35:30 -0600 Subject: [PATCH 10/18] Serial monitor debugging --- src/main.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 5b0ce93..027338e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -501,8 +501,14 @@ void loop1() { Serial.print("Charging State: "); if (chargingState == ChargingState::CHARGING) { Serial.println("CHARGING"); - } else { + } else if (chargingState == ChargingState::DISABLED) { Serial.println("CHARGING DISABLED"); + } else if (chargingState == ChargingState::READY) { + Serial.println("CHARGING READY"); + } else if (chargingState == ChargingState::COMPLETE) { + Serial.println("CHARGING COMPLETE"); + } else { + Serial.println("CHARGING FAULT"); } ReadBMS::logBalancingState(bmsSnapshot, Serial); @@ -536,5 +542,14 @@ void loop1() { Serial.println("CAN0 SOC message sent"); } + // CAN msg debugging + Serial.print("SOC: "); + Serial.println(soc.minSOC, 3); + Serial.print("Min cell voltage: "); + Serial.println(soc.minCellMv); + Serial.print("Max cell voltage: "); + Serial.println(soc.maxCellMv); + Serial.print("Highest Temp: "); + Serial.println(encodeHighestTempC(pollSnapshot)); } -} +} \ No newline at end of file From 8d0126d992befb24a1bdd1a1842ddff918948053 Mon Sep 17 00:00:00 2001 From: levi-potter Date: Wed, 10 Jun 2026 21:18:45 -0600 Subject: [PATCH 11/18] update temperature tolerances per rules --- include/CONSTANTS.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index 0ed7c2e..b280326 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -46,10 +46,10 @@ namespace constants { constexpr uint16_t kBalanceMaxCellMv = 5500; constexpr float kTempWarningMinC = 5.0f; - constexpr float kTempGoodMaxC = 55.0f; - constexpr uint8_t kTempChargingFaultC = 65; - constexpr float kTempWarningMaxC = 75.0f; // if cells reach 75 C then the BMS should error - constexpr float kTempExhaustedMaxC = 80.0f; // the cells should not go any higher than this per the datasheet + constexpr float kTempGoodMaxC = 50.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; From ee55b6723298706cec09d568523201124f418e5e Mon Sep 17 00:00:00 2001 From: levi-potter Date: Sat, 13 Jun 2026 16:16:49 -0600 Subject: [PATCH 12/18] charging fault sends msg to stop charger --- src/main.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 027338e..bdcce30 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -423,7 +423,7 @@ void loop1() { #ifdef COMMUNICATE_WITH_CHARGER if (gCan0Ready && (now - lastChargerControlMessageMs >= constants::kCanChargerControlIntervalMs) && - (chargingState == ChargingState::CHARGING || chargingState == ChargingState::COMPLETE)) { + (chargingState == ChargingState::CHARGING || chargingState == ChargingState::COMPLETE || chargingState == ChargingState::FAULT)) { lastChargerControlMessageMs = now; float targetAmperage = 0.0f; @@ -440,6 +440,11 @@ void loop1() { targetAmperage = 0.0f; chargerControl = MCP2517Can::ChargerControl::ChargerClose; break; + case ChargingState::FAULT: + // send msg to charger to STOP charging + targetAmperage = 0.0f; + chargerControl = MCP2517Can::ChargerControl::ChargerClose; + break; default: break; } From 8f4fb6ea2c659eb9204e849d172a8f779ceb3b08 Mon Sep 17 00:00:00 2001 From: levi-potter Date: Sat, 13 Jun 2026 20:43:03 -0600 Subject: [PATCH 13/18] update shutdown pin and charge enable pin; reworked charging logic --- include/CONSTANTS.h | 28 ++++++------ include/PINS.h | 10 ++-- src/JboxIO.cpp | 8 ---- src/main.cpp | 109 +++++++++++++++++++++++++++++--------------- 4 files changed, 92 insertions(+), 63 deletions(-) diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index b280326..1731750 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -19,16 +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 = 1000; constexpr uint32_t kCanBitRate = 250000; - 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 = 250; + 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; @@ -47,7 +50,7 @@ namespace constants { constexpr float kTempWarningMinC = 5.0f; constexpr float kTempGoodMaxC = 50.0f; - constexpr uint8_t kTempChargingFaultC = 60; // stop charging if max cell temp reaches this value + constexpr uint8_t kTempChargingFaultC = 55; // 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; @@ -55,11 +58,10 @@ namespace constants { constexpr std::size_t kLedCount = 13; constexpr uint8_t kLedBrightness = 32; - // 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 + // 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 float kStartChargeA = 9.0f; // 1.0C begining charging amperage + constexpr uint16_t kStartChargeA = 9; // 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/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/main.cpp b/src/main.cpp index bdcce30..0ddbd15 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -14,8 +14,6 @@ bool core1_separate_stack = true; -#define COMMUNICATE_WITH_CHARGER - // #define BALANCE_MODULES_INDEPENDENTLY namespace { @@ -47,14 +45,18 @@ namespace { // 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 + FAULT // Fault detected send msg to charger to STOP charging }; ChargingState chargingState = ChargingState::DISABLED; + // init charge enable pin state + bool charge_enabled = false; + uint8_t encodeAggregateStatus(StatusMode mode) { switch (mode) { case StatusMode::GOOD: @@ -227,30 +229,30 @@ namespace { } // BMS CAN msg for Elcon charger communication - MCP2517Can::Message buildCanChargerControlMessage(float maxChargingVoltageV, float maxChargingCurrentA, bool chargerControl, bool chargerMode) { + 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.0f); + uint16_t rawMaxChargingVoltageV = static_cast(maxChargingVoltageV * 10); // convert amperage to deciamps - uint16_t rawMaxChargingCurrentA = static_cast(maxChargingCurrentA * 10.0f); + uint16_t rawMaxChargingCurrentA = static_cast(maxChargingCurrentA * 10); // Max allowable charging terminal - message.data[0] = (rawMaxChargingVoltageV >> 8) & 0xFF; // high byte - message.data[1] = rawMaxChargingVoltageV & 0xFF; // low byte + message.data[0] = rawMaxChargingVoltageV >> 8; // 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 + message.data[2] = rawMaxChargingCurrentA >> 8; // high byte + message.data[3] = rawMaxChargingCurrentA & 0xFF; // low byte // Control - message.data[4] = chargerControl ? 0x00 : 0x01; + message.data[4] = chargerControl; // Working status control - message.data[5] = chargerMode ? 0x00 : 0x01; + message.data[5] = chargerMode; // reserved message.data[6] = 0x00; @@ -269,7 +271,7 @@ namespace { pinMode(CAN0_INT, INPUT_PULLUP); digitalWrite(CAN0_CS, HIGH); } -} // namespace +} // namespace void setup() { mutex_init(&gBmsDataMutex); @@ -299,7 +301,15 @@ void loop() { lastPollMs = now; SystemStatuses statusesForOutput{}; - // TODO read drive and charge enable pin - Not sure how this effects status yet + // update charge enable pin + uint16_t chargePinValue = analogRead(CHARGE_ENABLE_SENSE); + + if (chargePinValue > constants::kChargerPinConsideredHigh) { + charge_enabled = true; + } + else { + charge_enabled = false; + } mutex_enter_blocking(&gBmsDataMutex); @@ -363,7 +373,7 @@ void loop1() { // 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) { + } else if (chargingState == ChargingState::IDLE) { // update charger mode chargingState = ChargingState::READY; } @@ -377,6 +387,16 @@ void loop1() { } } + // if charge was enabled by pin, and the pin changes, then set charging status to fault to send msg to shut off charger + if (!charge_enabled && + (chargingState == ChargingState::IDLE || + chargingState == ChargingState::READY || + chargingState == ChargingState::CHARGING || + chargingState == ChargingState::COMPLETE)) + { + chargingState = ChargingState::FAULT; + } + // if charger status CAN msg was not recieved for 2 seconds, turn off charging mode if (now - lastChargerTimeoutMs >= constants::kCanChargerTimeOutMs) { chargingState = ChargingState::DISABLED; @@ -389,7 +409,7 @@ void loop1() { SystemStatuses statusesSnapshot{}; ReadBMS::PollData pollSnapshot{}; - // get the lastest SOC readings + // get the lastest SOC and BMS status readings mutex_enter_blocking(&gBmsDataMutex); statusesSnapshot = gSystemStatuses; soc = readBms.pollSOC(); @@ -397,9 +417,18 @@ void loop1() { mutex_exit(&gBmsDataMutex); switch (static_cast(chargingState)) { + case ChargingState::DISABLED: + // update charge mode from charge enable pin + if (charge_enabled) { + chargingState = ChargingState::IDLE; + } + break; case ChargingState::READY: - // check if safe to charge by checking voltages and temps - if ((soc.maxCellMv < constants::kCellVoltageGoodMaxMv) && (statusesSnapshot.temp == StatusMode::GOOD)) { + // 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; @@ -412,21 +441,35 @@ void loop1() { if (encodeHighestTempC(pollSnapshot) >= constants::kTempChargingFaultC) { chargingState = ChargingState::FAULT; } + // for safety, redundant check of BMS status + if (statusesSnapshot.BMS != StatusMode::GOOD) { + chargingState = ChargingState::FAULT; + } + 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); + + can0.send(fault_msg); + } break; default: break; } } - // send charging CAN msg - // Only send this msg if charging state is CHARGING or COMPLETE - #ifdef COMMUNICATE_WITH_CHARGER + // 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 || chargingState == ChargingState::FAULT)) { + (chargingState == ChargingState::CHARGING || chargingState == ChargingState::COMPLETE)) { lastChargerControlMessageMs = now; - float targetAmperage = 0.0f; + uint16_t targetAmperage = 0; bool chargerControl = MCP2517Can::ChargerControl::ChargerClose; // should always be in charging mode bool chargerMode = MCP2517Can::ChargingMode::ChargingMode; @@ -437,12 +480,8 @@ void loop1() { chargerControl = MCP2517Can::ChargerControl::ChargerStart; break; case ChargingState::COMPLETE: - targetAmperage = 0.0f; - chargerControl = MCP2517Can::ChargerControl::ChargerClose; - break; - case ChargingState::FAULT: // send msg to charger to STOP charging - targetAmperage = 0.0f; + targetAmperage = 0; chargerControl = MCP2517Can::ChargerControl::ChargerClose; break; default: @@ -459,8 +498,8 @@ void loop1() { Serial.println("Charger control message sent success"); } } - #endif + // Update balancing through serial GUI interface while (Serial.available() > 0) { const char ch = static_cast(Serial.read()); if (ch == '\r') { @@ -508,6 +547,8 @@ void loop1() { 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) { @@ -525,6 +566,7 @@ void loop1() { } } + // send BMS statues and basic information through CAN if (gCan0Ready && (now - lastCanStatusMs >= constants::kCanStatusIntervalMs)) { lastCanStatusMs = now; SystemStatuses statusesSnapshot{}; @@ -547,14 +589,5 @@ void loop1() { Serial.println("CAN0 SOC message sent"); } - // CAN msg debugging - Serial.print("SOC: "); - Serial.println(soc.minSOC, 3); - Serial.print("Min cell voltage: "); - Serial.println(soc.minCellMv); - Serial.print("Max cell voltage: "); - Serial.println(soc.maxCellMv); - Serial.print("Highest Temp: "); - Serial.println(encodeHighestTempC(pollSnapshot)); } } \ No newline at end of file From 414bd49889cdfe2b37eeccfce8ca1c8a9a3efe1e Mon Sep 17 00:00:00 2001 From: levi-potter Date: Mon, 15 Jun 2026 09:57:27 -0500 Subject: [PATCH 14/18] charging status response CAN msg serial monitoring debugging --- src/main.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 0ddbd15..5fba11a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -37,6 +37,7 @@ namespace { 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; @@ -261,6 +262,23 @@ namespace { 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.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); @@ -377,6 +395,11 @@ void loop1() { // update charger mode chargingState = ChargingState::READY; } + // print charger status via serial for debugging + if ((now - lastChargerLoggingMs >= 1000) && Serial.available()) { + lastChargerLoggingMs = now; + printChargerStatus(rmsg); + } break; case MCP2517Can::CanMsgId::MotorControlCommand: // BMS is in drive mode so disable charging From cee4e7ae33a65faf6815e06a078cd15f1676e872 Mon Sep 17 00:00:00 2001 From: levi-potter Date: Tue, 16 Jun 2026 12:15:34 -0400 Subject: [PATCH 15/18] charger shutdown fixed --- include/CONSTANTS.h | 4 ++-- src/main.cpp | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index 1731750..d85cf0c 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -24,7 +24,7 @@ namespace constants { // interval BMS sends data via Serial constexpr uint32_t kLogIntervalMs = 2000; // interval BMS scans modules - constexpr uint32_t kModuleScanIntervalMs = 1000; + constexpr uint32_t kModuleScanIntervalMs = 50; constexpr uint32_t kCanBitRate = 250000; constexpr uint32_t kCanStatusIntervalMs = 1000; // Rate at which data CAN status information is sent @@ -50,7 +50,7 @@ namespace constants { constexpr float kTempWarningMinC = 5.0f; constexpr float kTempGoodMaxC = 50.0f; - constexpr uint8_t kTempChargingFaultC = 55; // stop charging if max cell temp reaches this value + 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; diff --git a/src/main.cpp b/src/main.cpp index 5fba11a..cee6e48 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -439,6 +439,11 @@ void loop1() { pollSnapshot = readBms.data(); mutex_exit(&gBmsDataMutex); + // if BMS has faulted the charger should turn off + if (statusesSnapshot.BMS != StatusMode::GOOD) { + chargingState = ChargingState::FAULT; + } + switch (static_cast(chargingState)) { case ChargingState::DISABLED: // update charge mode from charge enable pin From b247a929cecaecf3d22dafe099437fce57ba258b Mon Sep 17 00:00:00 2001 From: levi-potter Date: Tue, 16 Jun 2026 20:56:31 -0400 Subject: [PATCH 16/18] Charger debugging ONLY!!! --- include/CONSTANTS.h | 2 +- include/SystemStatus.h | 3 +- platformio.ini | 2 +- src/main.cpp | 81 ++++++++++++++++++++++-------------------- 4 files changed, 46 insertions(+), 42 deletions(-) diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index d85cf0c..7205506 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -24,7 +24,7 @@ namespace constants { // interval BMS sends data via Serial constexpr uint32_t kLogIntervalMs = 2000; // interval BMS scans modules - constexpr uint32_t kModuleScanIntervalMs = 50; + constexpr uint32_t kModuleScanIntervalMs = 100; constexpr uint32_t kCanBitRate = 250000; constexpr uint32_t kCanStatusIntervalMs = 1000; // Rate at which data CAN status information is sent diff --git a/include/SystemStatus.h b/include/SystemStatus.h index 64e503c..1e16a4c 100644 --- a/include/SystemStatus.h +++ b/include/SystemStatus.h @@ -115,7 +115,8 @@ inline StatusMode evaluateTempStatus(const ModuleReadings& module) { const float tempC = module.thermistorTempsC[i]; StatusMode thermistorStatus = StatusMode::BAD_DATA; - if (!isnan(tempC) && tempC >= constants::kTempWarningMinC) { + // FIXME Hacks to IGNORE DISCONNECTED THEMISTORS!!!! + if (!isnan(tempC)) { ++validThermistorCount; if (tempC > constants::kTempExhaustedMaxC) { diff --git a/platformio.ini b/platformio.ini index 8ac12d3..fd6b135 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,6 +15,7 @@ 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 @@ -22,4 +23,3 @@ lib_deps = fastled/FastLED@^3.10.3 pierremolinaro/acan2517 pierremolinaro/acan2517FD - throwtheswitch/Unity@^2.6.1 diff --git a/src/main.cpp b/src/main.cpp index cee6e48..9804ebf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -256,8 +256,8 @@ namespace { message.data[5] = chargerMode; // reserved - message.data[6] = 0x00; - message.data[7] = 0x00; + // message.data[6] = 0x00; + // message.data[7] = 0x00; return message; } @@ -268,6 +268,7 @@ namespace { 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"); @@ -320,14 +321,19 @@ void loop() { SystemStatuses statusesForOutput{}; // update charge enable pin - uint16_t chargePinValue = analogRead(CHARGE_ENABLE_SENSE); + // uint16_t chargePinValue = analogRead(CHARGE_ENABLE_SENSE); - if (chargePinValue > constants::kChargerPinConsideredHigh) { - charge_enabled = true; - } - else { - charge_enabled = false; - } + // if (chargePinValue > constants::kChargerPinConsideredHigh) { + // charge_enabled = true; + // } + // else { + // charge_enabled = false; + // } + + // // shutdown charger if charging is complete + // if (chargingState == ChargingState::COMPLETE) { + // charge_enabled = false; + // } mutex_enter_blocking(&gBmsDataMutex); @@ -384,24 +390,28 @@ void loop1() { 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; - } else if (chargingState == ChargingState::IDLE) { + // chargingState = ChargingState::FAULT; + Serial.println("Charger flag fault: 0x"); + Serial.print(rmsg.data[4], 16); + } + + if (chargingState == ChargingState::DISABLED) { // update charger mode chargingState = ChargingState::READY; } - // print charger status via serial for debugging - if ((now - lastChargerLoggingMs >= 1000) && Serial.available()) { - lastChargerLoggingMs = now; - printChargerStatus(rmsg); - } + + 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; @@ -410,16 +420,6 @@ void loop1() { } } - // if charge was enabled by pin, and the pin changes, then set charging status to fault to send msg to shut off charger - if (!charge_enabled && - (chargingState == ChargingState::IDLE || - chargingState == ChargingState::READY || - chargingState == ChargingState::CHARGING || - chargingState == ChargingState::COMPLETE)) - { - chargingState = ChargingState::FAULT; - } - // if charger status CAN msg was not recieved for 2 seconds, turn off charging mode if (now - lastChargerTimeoutMs >= constants::kCanChargerTimeOutMs) { chargingState = ChargingState::DISABLED; @@ -442,15 +442,10 @@ void loop1() { // if BMS has faulted the charger should turn off if (statusesSnapshot.BMS != StatusMode::GOOD) { chargingState = ChargingState::FAULT; + Serial.println("Charger fault from BMS fault 449"); } switch (static_cast(chargingState)) { - case ChargingState::DISABLED: - // update charge mode from charge enable pin - if (charge_enabled) { - chargingState = ChargingState::IDLE; - } - break; 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) && @@ -468,10 +463,12 @@ void loop1() { // 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: @@ -483,7 +480,9 @@ void loop1() { MCP2517Can::ChargerControl::ChargerClose, MCP2517Can::ChargingMode::ChargingMode); - can0.send(fault_msg); + if (can0.send(fault_msg)) { + Serial.println("Charger shutdown msg sent"); + } } break; default: @@ -562,6 +561,7 @@ void loop1() { bmsSnapshot = readBms.captureLogSnapshot(); mutex_exit(&gBmsDataMutex); + Serial.println(); Serial.print("status BMS: "); Serial.println(statusModeName(statusesSnapshot.BMS)); Serial.print("status board: "); @@ -584,14 +584,15 @@ void loop1() { } else { Serial.println("CHARGING FAULT"); } + Serial.println(); - ReadBMS::logBalancingState(bmsSnapshot, Serial); + // ReadBMS::logBalancingState(bmsSnapshot, Serial); - ReadBMS::logConnectedModules(bmsSnapshot, Serial); - if (logSiliconIds) { - ReadBMS::logModuleSiliconIds(bmsSnapshot, Serial); - logCycleCount = 0; - } + // ReadBMS::logConnectedModules(bmsSnapshot, Serial); + // if (logSiliconIds) { + // ReadBMS::logModuleSiliconIds(bmsSnapshot, Serial); + // logCycleCount = 0; + // } } // send BMS statues and basic information through CAN @@ -610,11 +611,13 @@ void loop1() { const MCP2517Can::Message statusMessage = buildCanStatusMessage(statusesSnapshot, pollSnapshot); 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(); } } From 72d12aadfa82b17f7d3e803d6abee95fa60830df Mon Sep 17 00:00:00 2001 From: levi-potter Date: Wed, 17 Jun 2026 08:56:59 -0400 Subject: [PATCH 17/18] charge enable removed --- include/CONSTANTS.h | 4 ++-- include/SystemStatus.h | 3 +-- src/main.cpp | 41 +++++++---------------------------------- 3 files changed, 10 insertions(+), 38 deletions(-) diff --git a/include/CONSTANTS.h b/include/CONSTANTS.h index 7205506..374fce5 100644 --- a/include/CONSTANTS.h +++ b/include/CONSTANTS.h @@ -24,7 +24,7 @@ namespace constants { // interval BMS sends data via Serial constexpr uint32_t kLogIntervalMs = 2000; // interval BMS scans modules - constexpr uint32_t kModuleScanIntervalMs = 100; + constexpr uint32_t kModuleScanIntervalMs = 500; constexpr uint32_t kCanBitRate = 250000; constexpr uint32_t kCanStatusIntervalMs = 1000; // Rate at which data CAN status information is sent @@ -62,6 +62,6 @@ namespace constants { 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 = 9; // 0.5C charging amperage + constexpr uint16_t kStartChargeA = 1; // 0.5C charging amperage } // namespace constants diff --git a/include/SystemStatus.h b/include/SystemStatus.h index 1e16a4c..64e503c 100644 --- a/include/SystemStatus.h +++ b/include/SystemStatus.h @@ -115,8 +115,7 @@ inline StatusMode evaluateTempStatus(const ModuleReadings& module) { const float tempC = module.thermistorTempsC[i]; StatusMode thermistorStatus = StatusMode::BAD_DATA; - // FIXME Hacks to IGNORE DISCONNECTED THEMISTORS!!!! - if (!isnan(tempC)) { + if (!isnan(tempC) && tempC >= constants::kTempWarningMinC) { ++validThermistorCount; if (tempC > constants::kTempExhaustedMaxC) { diff --git a/src/main.cpp b/src/main.cpp index 9804ebf..8d0b415 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -55,9 +55,6 @@ namespace { ChargingState chargingState = ChargingState::DISABLED; - // init charge enable pin state - bool charge_enabled = false; - uint8_t encodeAggregateStatus(StatusMode mode) { switch (mode) { case StatusMode::GOOD: @@ -252,13 +249,6 @@ namespace { // Control message.data[4] = chargerControl; - // Working status control - message.data[5] = chargerMode; - - // reserved - // message.data[6] = 0x00; - // message.data[7] = 0x00; - return message; } @@ -320,21 +310,6 @@ void loop() { lastPollMs = now; SystemStatuses statusesForOutput{}; - // update charge enable pin - // uint16_t chargePinValue = analogRead(CHARGE_ENABLE_SENSE); - - // if (chargePinValue > constants::kChargerPinConsideredHigh) { - // charge_enabled = true; - // } - // else { - // charge_enabled = false; - // } - - // // shutdown charger if charging is complete - // if (chargingState == ChargingState::COMPLETE) { - // charge_enabled = false; - // } - mutex_enter_blocking(&gBmsDataMutex); // Poll the slave boards, and get the latest readings @@ -401,9 +376,7 @@ void loop1() { // chargingState = ChargingState::FAULT; Serial.println("Charger flag fault: 0x"); Serial.print(rmsg.data[4], 16); - } - - if (chargingState == ChargingState::DISABLED) { + } else if (chargingState == ChargingState::DISABLED) { // update charger mode chargingState = ChargingState::READY; } @@ -586,13 +559,13 @@ void loop1() { } Serial.println(); - // ReadBMS::logBalancingState(bmsSnapshot, Serial); + ReadBMS::logBalancingState(bmsSnapshot, Serial); - // ReadBMS::logConnectedModules(bmsSnapshot, Serial); - // if (logSiliconIds) { - // ReadBMS::logModuleSiliconIds(bmsSnapshot, Serial); - // logCycleCount = 0; - // } + ReadBMS::logConnectedModules(bmsSnapshot, Serial); + if (logSiliconIds) { + ReadBMS::logModuleSiliconIds(bmsSnapshot, Serial); + logCycleCount = 0; + } } // send BMS statues and basic information through CAN From c3f3fdc52d88fe107c7e0a93b8fddefeda864286 Mon Sep 17 00:00:00 2001 From: levi-potter Date: Wed, 17 Jun 2026 10:55:33 -0400 Subject: [PATCH 18/18] debugging tools --- src/main.cpp | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 8d0b415..4b11de2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -412,12 +412,6 @@ void loop1() { pollSnapshot = readBms.data(); mutex_exit(&gBmsDataMutex); - // if BMS has faulted the charger should turn off - if (statusesSnapshot.BMS != StatusMode::GOOD) { - chargingState = ChargingState::FAULT; - Serial.println("Charger fault from BMS fault 449"); - } - 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