From f7fa20920eadd6c2e9f097bfd56f108e0d0a347f Mon Sep 17 00:00:00 2001 From: Armin Date: Mon, 23 Sep 2024 14:16:04 +0200 Subject: [PATCH] Support for multi BMS --- .github/workflows/TestCompile.yml | 6 +- JK-BMSToPylontechCAN/JK-BMS.h | 178 +++++++---------- JK-BMSToPylontechCAN/JK-BMS.hpp | 187 +++++++++++++----- JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino | 132 ++++++++----- JK-BMSToPylontechCAN/JK-BMS_LCD.hpp | 28 +-- JK-BMSToPylontechCAN/Pylontech_CAN.h | 75 ++++--- README.md | 35 +++- 7 files changed, 386 insertions(+), 255 deletions(-) diff --git a/.github/workflows/TestCompile.yml b/.github/workflows/TestCompile.yml index 151cb4a..7a93327 100644 --- a/.github/workflows/TestCompile.yml +++ b/.github/workflows/TestCompile.yml @@ -28,7 +28,7 @@ jobs: - arduino:avr:nano - arduino:avr:uno - arduino:avr:uno|STANDALONE_TEST - - arduino:avr:uno|LOCAL_DEBUG + - arduino:avr:uno|DEBUG - arduino:avr:uno|DISPLAY_ALWAYS_ON - arduino:avr:uno|USE_NO_LCD - MightyCore:avr:644 @@ -38,9 +38,9 @@ jobs: build-properties: All: -DSTANDALONE_TEST - - arduino-boards-fqbn: arduino:avr:uno|LOCAL_DEBUG + - arduino-boards-fqbn: arduino:avr:uno|DEBUG build-properties: - All: -DLOCAL_DEBUG + All: -DDEBUG - arduino-boards-fqbn: arduino:avr:uno|DISPLAY_ALWAYS_ON build-properties: diff --git a/JK-BMSToPylontechCAN/JK-BMS.h b/JK-BMSToPylontechCAN/JK-BMS.h index 3f89e33..8fe598e 100644 --- a/JK-BMSToPylontechCAN/JK-BMS.h +++ b/JK-BMSToPylontechCAN/JK-BMS.h @@ -50,7 +50,7 @@ #define JK_FRAME_START_BYTE_1 0x57 #define JK_FRAME_END_BYTE 0x68 -extern uint8_t JKReplyFrameBuffer[350]; // The raw big endian data as received from JK BMS +extern uint8_t JKReplyFrameBuffer[350]; // The raw big endian data as received from JK BMS extern char sUpTimeString[12]; // "1000D23H12M" is 11 bytes long extern bool sUpTimeStringMinuteHasChanged; @@ -107,7 +107,7 @@ struct CellStatisticsStruct { uint32_t LastPrintedBalancingCount; // For printing with printJKDynamicInfo() }; -#define MINIMUM_BALANCING_COUNT_FOR_DISPLAY 60 // 120 seconds / 2 minutes of balancing +#define MINIMUM_BALANCING_COUNT_FOR_DISPLAY 60 // 120 seconds / 2 minutes of balancing #endif // NO_CELL_STATISTICS #define JK_BMS_FRAME_HEADER_LENGTH 11 @@ -116,7 +116,7 @@ struct CellStatisticsStruct { #define JK_BMS_FRAME_INDEX_OF_CELL_INFO_LENGTH (JK_BMS_FRAME_HEADER_LENGTH + 1) // +1 for token 0x79 #define MINIMAL_JK_BMS_FRAME_LENGTH 19 -#define TIMEOUT_MILLIS_FOR_FRAME_REPLY 100 // I measured 26 ms between request end and end of received 273 byte result +#define TIMEOUT_MILLIS_FOR_FRAME_REPLY 100 // I measured 26 ms between request end and end of received 273 byte result #if TIMEOUT_MILLIS_FOR_FRAME_REPLY > MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS #error "TIMEOUT_MILLIS_FOR_FRAME_REPLY must be smaller than MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS to detect timeouts" #endif @@ -151,7 +151,7 @@ struct JKComputedDataStruct { int16_t TemperatureMaximum; // Computed value uint16_t TotalCapacityAmpereHour; - uint16_t RemainingCapacityAmpereHour; // Computed value + uint16_t RemainingCapacityAmpereHour; // Computed value uint16_t BatteryFullVoltage10Millivolt; // Computed by BMS! Is ActualNumberOfCellInfoEntries * CellOvervoltageProtectionMillivolt uint16_t BatteryEmptyVoltage10Millivolt; uint16_t BatteryVoltage10Millivolt; @@ -163,6 +163,12 @@ struct JKComputedDataStruct { int16_t BatteryLoadPower; // Watt Computed value, Charging is positive discharging is negative int32_t BatteryCapacityAsAccumulator10MilliAmpere; // 500 Ah = 180,000,000 10MilliAmpereSeconds. Pre-computed capacity to compare with accumulator value. bool BMSIsStarting; // True if SOC and Cycles are both 0, for around 16 seconds during JK-BMS startup. +#if defined(HANDLE_MULTIPLE_BMS) + uint16_t ChargeOvercurrentProtectionAmpere; + uint16_t DischargeOvercurrentProtectionAmpere; + uint16_t AlarmsAsWord; + uint16_t StatusAsWord; +#endif }; #define AMPERE_HOUR_AS_ACCUMULATOR_10_MILLIAMPERE (3600L * 100 * MILLIS_IN_ONE_SECOND / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) // 180000 @@ -176,17 +182,14 @@ struct JKLastPrintedDataStruct { int32_t BatteryCapacityAccumulator10MilliAmpere; // For CSV line to print every 1% }; -/* - * Only for documentation - */ union BMSStatusUnion { uint16_t StatusAsWord; struct { - uint8_t ReservedStatusHighByte; // This is the low byte of StatusAsWord, but it was sent as high byte of status - bool ChargeMosFetActive :1; // 0x01 // Is disabled e.g. on over current or temperature - bool DischargeMosFetActive :1; // 0x02 // Is disabled e.g. on over current or temperature - bool BalancerActive :1; // 0x04 - bool BatteryDown :1; // 0x08 + uint8_t ReservedStatusHighByte; // This is the low byte of StatusAsWord, but it was sent as high byte of status + bool ChargeMosFetActive :1; // 0x01 // Is disabled e.g. on over current or temperature + bool DischargeMosFetActive :1; // 0x02 // Is disabled e.g. on over current or temperature + bool BalancerActive :1; // 0x04 + bool BatteryDown :1; // 0x08 uint8_t ReservedStatus :4; } StatusBits; }; @@ -227,6 +230,35 @@ union BMSStatusUnion { #define MASK_OF_DISCHARGING_UNDERVOLTAGE_ALARM_UNSWAPPED 0x0400 #define INDEX_NO_ALARM 0xFF +union BatteryAlarmFlagsUnion { // 0x8B + uint16_t AlarmsAsWord; + struct { + // High byte of alarms sent + bool Sensor2OvertemperatureAlarm :1; // 0x0100 + bool Sensor1Or2UndertemperatureAlarm :1; // 0x0200 Disables charging, but Has no effect on discharging + bool CellOvervoltageAlarm :1; // 0x0400 + bool CellUndervoltageAlarm :1; + bool _309_A_ProtectionAlarm :1; // 0x1000 + bool _309_B_ProtectionAlarm :1; + bool Reserved1Alarm :1; // Two highest bits are reserved + bool Reserved2Alarm :1; + + // Low byte of alarms sent + bool LowCapacityAlarm :1; // 0x0001 + bool PowerMosFetOvertemperatureAlarm :1; + bool ChargeOvervoltageAlarm :1; // 0x0004 This happens quite often, if battery charging is approaching 100 % + bool DischargeUndervoltageAlarm :1; + bool Sensor1Or2OvertemperatureAlarm :1; // 0x0010 - Affects the charging/discharging MosFet state, not the enable flags + /* + * Set with delay of (Dis)ChargeOvercurrentDelaySeconds / "OCP Delay(S)" seconds initially or on retry. + * Retry is done after "OCPR Time(S)" + */ + bool ChargeOvercurrentAlarm :1; // 0x0020 - Set with delay of ChargeOvercurrentDelaySeconds seconds initially or on retry + bool DischargeOvercurrentAlarm :1; // 0x0040 - Set with delay of DischargeOvercurrentDelaySeconds seconds initially or on retry + bool CellVoltageDifferenceAlarm :1; // 0x0080 + } AlarmBits; +}; + struct JKReplyStruct { uint8_t TokenTemperaturePowerMosFet; // 0x80 uint16_t TemperaturePowerMosFet; // 99 = 99 degree Celsius, 100 = 100, 101 = -1, 140 = -40 @@ -251,52 +283,10 @@ struct JKReplyStruct { uint8_t TokenNumberOfBatteryCells; // 0x8A uint16_t NumberOfBatteryCells; uint8_t TokenBatteryAlarm; // 0x8B - - union { - uint16_t AlarmsAsWord; - struct { - uint8_t AlarmsHighByte; // This is the low byte of AlarmsAsWord, but it was sent as high byte of alarms - uint8_t AlarmsLowByte; // This is the high byte of AlarmsAsWord, but it was sent as low byte of alarms - } AlarmBytes; - struct { - // High byte of alarms sent, but low byte of AlarmsAsWord - bool Sensor2OvertemperatureAlarm :1; // 0x0100 - bool Sensor1Or2UndertemperatureAlarm :1; // 0x0200 Disables charging, but Has no effect on discharging - bool CellOvervoltageAlarm :1; // 0x0400 - bool CellUndervoltageAlarm :1; // 0x0800 - bool _309_A_ProtectionAlarm :1; // 0x1000 - bool _309_B_ProtectionAlarm :1; - bool Reserved1Alarm :1; // Two highest bits are reserved - bool Reserved2Alarm :1; // 0x8000 - - // Low byte of alarms sent, but high byte of AlarmsAsWord - bool LowCapacityAlarm :1; // 0x0001 - bool PowerMosFetOvertemperatureAlarm :1; - bool ChargeOvervoltageAlarm :1; // 0x0004 This happens quite often, if battery charging is approaching 100 % - bool DischargeUndervoltageAlarm :1; // 0x0008 Discharging undervoltage forces SOC to 0 and a few seconds later switches off discharge mosfet - bool Sensor1Or2OvertemperatureAlarm :1; // 0x0010 - Affects the charging/discharging MosFet state, not the enable flags - /* - * Set with delay of (Dis)ChargeOvercurrentDelaySeconds / "OCP Delay(S)" seconds initially or on retry. - * Retry is done after "OCPR Time(S)" - */ - bool ChargeOvercurrentAlarm :1; // 0x0020 - Set with delay of ChargeOvercurrentDelaySeconds seconds initially or on retry - bool DischargeOvercurrentAlarm :1; // 0x0040 - Set with delay of DischargeOvercurrentDelaySeconds seconds initially or on retry - bool CellVoltageDifferenceAlarm :1; // 0x0080 - } AlarmBits; - } AlarmUnion; + union BatteryAlarmFlagsUnion BatteryAlarmFlags; uint8_t TokenBatteryStatus; // 0x8C - union { - uint16_t StatusAsWord; - struct { - uint8_t ReservedStatusHighByte; // This is the low byte of StatusAsWord, but it was sent as high byte of status - bool ChargeMosFetActive :1; // 0x01 // Is disabled e.g. on over current or temperature - bool DischargeMosFetActive :1; // 0x02 // Is disabled e.g. on over current or temperature - bool BalancerActive :1; // 0x04 - bool BatteryDown :1; // 0x08 - uint8_t ReservedStatus :4; - } StatusBits; - } BMSStatus; + union BMSStatusUnion BMSStatus; uint8_t TokenBatteryOvervoltageProtection10Millivolt; // 0x8E uint16_t BatteryOvervoltageProtection10Millivolt; // 1000 to 15000 BMS computed: # of cells * CellOvervoltageProtectionMillivolt @@ -436,67 +426,24 @@ struct JKReplyStruct { */ struct JKLastReplyStruct { uint8_t SOCPercent; // 0-100% 0x85 - union { // 0x8B - uint16_t AlarmsAsWord; - struct { - // High byte of alarms sent - bool Sensor2OvertemperatureAlarm :1; // 0x0100 - bool Sensor1Or2UndertemperatureAlarm :1; // 0x0200 Disables charging, but Has no effect on discharging - bool CellOvervoltageAlarm :1; // 0x0400 - bool CellUndervoltageAlarm :1; - bool _309_A_ProtectionAlarm :1; // 0x1000 - bool _309_B_ProtectionAlarm :1; - bool Reserved1Alarm :1; // Two highest bits are reserved - bool Reserved2Alarm :1; - - // Low byte of alarms sent - bool LowCapacityAlarm :1; // 0x0001 - bool PowerMosFetOvertemperatureAlarm :1; - bool ChargeOvervoltageAlarm :1; // 0x0004 This happens quite often, if battery charging is approaching 100 % - bool DischargeUndervoltageAlarm :1; - bool Sensor1Or2OvertemperatureAlarm :1; // 0x0010 - Affects the charging/discharging MosFet state, not the enable flags - /* - * Set with delay of (Dis)ChargeOvercurrentDelaySeconds / "OCP Delay(S)" seconds initially or on retry. - * Retry is done after "OCPR Time(S)" - */ - bool ChargeOvercurrentAlarm :1; // 0x0020 - Set with delay of ChargeOvercurrentDelaySeconds seconds initially or on retry - bool DischargeOvercurrentAlarm :1; // 0x0040 - Set with delay of DischargeOvercurrentDelaySeconds seconds initially or on retry - bool CellVoltageDifferenceAlarm :1; // 0x0080 - } AlarmBits; - } AlarmUnion; - - union { // 0x8C - uint16_t StatusAsWord; - struct { - uint8_t ReservedStatusHighByte; // This is the low byte of StatusAsWord, but it was sent as high byte of status - bool ChargeMosFetActive :1; // 0x01 // Is disabled e.g. on over current or temperature - bool DischargeMosFetActive :1; // 0x02 // Is disabled e.g. on over current or temperature - bool BalancerActive :1; // 0x04 - bool BatteryDown :1; // 0x08 - uint8_t ReservedStatus :4; - } StatusBits; - } BMSStatus; - + union BatteryAlarmFlagsUnion BatteryAlarmFlags; + union BMSStatusUnion BMSStatus; uint32_t SystemWorkingMinutes; // Minutes 0xB6 }; /* * Return codes for uint8_t returns */ -#define JK_BMS_RECEIVE_ONGOING 0 // No requested or ongoing receiving +#define JK_BMS_RECEIVE_ONGOING 0 // No requested or ongoing receiving #define JK_BMS_RECEIVE_FINISHED 1 #define JK_BMS_RECEIVE_TIMEOUT 2 #define JK_BMS_RECEIVE_ERROR 3 // Received byte was not plausible -#if defined(HANDLE_MULTIPLE_BMS) -extern uint8_t NumbersOfBMSInstances; // The number of the BMS classes instantiated. -#endif - class JK_BMS { public: JK_BMS(); void init(uint8_t aTxPinNumber); - void requestJK_BMSStatusFrame( bool aDebugModeActive); + void requestJK_BMSStatusFrame(bool aDebugModeActive); void RequestStatusFrame(bool aDebugModeActive); uint8_t readJK_BMSStatusFrameByte(); uint8_t checkForReplyFromBMSOrTimeout(); @@ -544,13 +491,13 @@ class JK_BMS { * Flags for interface */ bool TimeoutJustDetected = false; // Is set to true at first detection of timeout and reset by beep timeout or receiving of a frame - bool JKBMSFrameHasTimeout; // True, as long as BMS timeout persists. + bool JKBMSFrameHasTimeout; // True, as long as BMS timeout persists. /* * sAlarmJustGetsActive is set and reset by detectAndPrintAlarmInfo() and reset by checkButtonPressForLCD() and beep handling. * Can also be set to true if 2 alarms are active and one of them gets inactive. If true, beep (with optional timeout) is generated. */ bool AlarmJustGetsActive = false; // True if alarm bits changed and any alarm is still active. False if alarm bits changed and no alarm is active. - bool AlarmActive = false; // True as long as any alarm is still active. False if no alarm is active. + bool AlarmActive = false; // True as long as any alarm is still active. False if no alarm is active. #if defined(USE_SERIAL_2004_LCD) uint8_t AlarmIndexToShowOnLCD = INDEX_NO_ALARM; // Index of current alarm to show with Alarm / Overview page. Set by detectAndPrintAlarmInfo() and reset on page switch. #endif @@ -558,13 +505,13 @@ class JK_BMS { /* * Size of reply is 291 bytes for 16 cells. sizeof(JKReplyStruct) is 221. */ - uint16_t ReplyFrameLength; // Received length of frame - uint16_t ReplyFrameBufferIndex = 0; // Index of next byte to write to array, except for last byte received. Starting with 0. + uint16_t ReplyFrameLength; // Received length of frame + uint16_t ReplyFrameBufferIndex; // Index of next byte to write to array, except for last byte received. Starting with 0. /* * BMS communication timeout */ - uint32_t MillisOfLastReceivedByte = 0; // For timeout detection + uint32_t MillisOfLastReceivedByte; // For timeout detection /* * Use a 115200 baud software serial for the short request frame. @@ -573,7 +520,18 @@ class JK_BMS { SoftwareSerialTX TxToJKBMS; #if defined(HANDLE_MULTIPLE_BMS) - uint8_t NumberOfBMS; // The number of the BMS this class is used for. Starting with 1. + uint8_t NumberOfThisBMS; // The number of the BMS this class is used for. Starting with 1. => Is index of next BMS in list :-). + static uint8_t sBMS_ArrayNextIndex; // = number of the BMS classes instantiated. + static JK_BMS *BMSArray[NUMBER_OF_SUPPORTED_BMS]; + static bool getAnyAlarm(); + static bool getAnyTimeout(); + static uint16_t getSumOfChargeOvercurrentProtectionAmpere(); + static uint16_t getSumOfDischargeOvercurrentProtectionAmpere(); + static int16_t getSumOfBattery10MilliAmpere(); + static uint16_t getSumOfTotalCapacityAmpereHour(); + static union BatteryAlarmFlagsUnion getOredBatteryAlarmFlags(); + static union BMSStatusUnion getOredBMSStatusFlags(); + #endif /* @@ -588,6 +546,6 @@ class JK_BMS { #if !defined(NO_CELL_STATISTICS) struct CellStatisticsStruct CellStatistics; #endif //NO_CELL_STATISTICS - }; + #endif // _JK_BMS_H diff --git a/JK-BMSToPylontechCAN/JK-BMS.hpp b/JK-BMSToPylontechCAN/JK-BMS.hpp index 61a179f..4d4664b 100644 --- a/JK-BMSToPylontechCAN/JK-BMS.hpp +++ b/JK-BMSToPylontechCAN/JK-BMS.hpp @@ -36,6 +36,12 @@ #include +#if defined(HANDLE_MULTIPLE_BMS) +# if !defined(NUMBER_OF_SUPPORTED_BMS) +#define NUMBER_OF_SUPPORTED_BMS 2 +# endif +#endif + #include "JK-BMS.h" #include "HexDump.hpp" // include sources for printBufferHex() @@ -48,10 +54,6 @@ #else #endif -#if defined(HANDLE_MULTIPLE_BMS) -uint8_t NumbersOfBMSInstances = 0; // The number of the BMS classes instantiated. -#endif - // This block must be located after the includes of other *.hpp files //#define LOCAL_DEBUG // This enables debug output only for this file - only for development #include "LocalDebugLevelStart.h" @@ -108,10 +110,16 @@ const char *const JK_BMSAlarmStringsArray[NUMBER_OF_DEFINED_ALARM_BITS] PROGMEM #define STR_HELPER(x) #x #define STR(x) STR_HELPER(x) -JK_BMS::JK_BMS() { #if defined(HANDLE_MULTIPLE_BMS) - NumbersOfBMSInstances++; - NumberOfBMS = NumbersOfBMSInstances; +JK_BMS *JK_BMS::BMSArray[NUMBER_OF_SUPPORTED_BMS]; +uint8_t JK_BMS::sBMS_ArrayNextIndex = 0; +#endif + +JK_BMS::JK_BMS() { // @suppress("Class members should be properly initialized") +#if defined(HANDLE_MULTIPLE_BMS) + BMSArray[sBMS_ArrayNextIndex] = this; + sBMS_ArrayNextIndex++; + NumberOfThisBMS = sBMS_ArrayNextIndex; #endif } @@ -134,7 +142,7 @@ void JK_BMS::requestJK_BMSStatusFrame(bool aDebugModeActive) { Serial.println(); #if defined(HANDLE_MULTIPLE_BMS) Serial.print(F("Send requestFrame to BMS ")); - Serial.println(NumberOfBMS); + Serial.println(NumberOfThisBMS); #else Serial.println(F("Send requestFrame to BMS")); #endif @@ -167,7 +175,7 @@ void JK_BMS::RequestStatusFrame(bool aDebugModeActive) { * Copy last complete reply and computed values for change determination */ lastJKReply.SOCPercent = JKAllReplyPointer->SOCPercent; - lastJKReply.AlarmUnion.AlarmsAsWord = JKAllReplyPointer->AlarmUnion.AlarmsAsWord; + lastJKReply.BatteryAlarmFlags.AlarmsAsWord = JKAllReplyPointer->BatteryAlarmFlags.AlarmsAsWord; lastJKReply.BMSStatus.StatusAsWord = JKAllReplyPointer->BMSStatus.StatusAsWord; lastJKReply.SystemWorkingMinutes = JKAllReplyPointer->SystemWorkingMinutes; @@ -264,7 +272,13 @@ uint8_t JK_BMS::checkForReplyFromBMSOrTimeout() { * All JK-BMS status frame data received */ if (JKBMSFrameHasTimeout) { +#if defined(HANDLE_MULTIPLE_BMS) + JK_INFO_PRINT(F("Successfully receiving first BMS ")); + JK_INFO_PRINT(NumberOfThisBMS); + JK_INFO_PRINTLN(F(" frame after communication timeout")); +#else JK_INFO_PRINTLN(F("Successfully receiving first BMS frame after communication timeout")); +#endif // First successful response frame after timeout :-) JKBMSFrameHasTimeout = false; TimeoutJustDetected = false; @@ -330,8 +344,8 @@ void JK_BMS::printJKReplyFrameBuffer() { uint8_t *tBufferAddress = JKReplyFrameBuffer; Serial.print(ReplyFrameBufferIndex + 1); #if defined(HANDLE_MULTIPLE_BMS) - Serial.print(F(" bytes received from BMS ")); - Serial.println(NumberOfBMS); + Serial.print(F(" bytes received from BMS ")); + Serial.println(NumberOfThisBMS); #else Serial.println(F(" bytes received")); #endif @@ -384,6 +398,76 @@ int32_t JK_BMS::getOnePercentCapacityAsAccumulator10Milliampere() { return (AMPERE_HOUR_AS_ACCUMULATOR_10_MILLIAMPERE / 100) * JKComputedData.TotalCapacityAmpereHour; } +#if defined(HANDLE_MULTIPLE_BMS) +bool JK_BMS::getAnyAlarm() { + for (uint_fast8_t i = 0; i < JK_BMS::sBMS_ArrayNextIndex; ++i) { + if (JK_BMS::BMSArray[i]->AlarmActive) { + return true; + } + } + return false; +} + +bool JK_BMS::getAnyTimeout() { + for (uint_fast8_t i = 0; i < JK_BMS::sBMS_ArrayNextIndex; ++i) { + if (JK_BMS::BMSArray[i]->JKBMSFrameHasTimeout) { + return true; + } + } + return false; +} + +uint16_t JK_BMS::getSumOfChargeOvercurrentProtectionAmpere() { + uint16_t tChargeOvercurrentProtectionAmpereSum = 0; + for (uint_fast8_t i = 0; i < JK_BMS::sBMS_ArrayNextIndex; ++i) { + tChargeOvercurrentProtectionAmpereSum += JK_BMS::BMSArray[i]->JKComputedData.ChargeOvercurrentProtectionAmpere; + } + return tChargeOvercurrentProtectionAmpereSum; +} + +uint16_t JK_BMS::getSumOfDischargeOvercurrentProtectionAmpere() { + uint16_t tDischargeOvercurrentProtectionAmpereSum = 0; + for (uint_fast8_t i = 0; i < JK_BMS::sBMS_ArrayNextIndex; ++i) { + tDischargeOvercurrentProtectionAmpereSum += JK_BMS::BMSArray[i]->JKComputedData.DischargeOvercurrentProtectionAmpere; + } + return tDischargeOvercurrentProtectionAmpereSum; +} + +int16_t JK_BMS::getSumOfBattery10MilliAmpere() { + int16_t tBattery10MilliAmpereSum = 0; + for (uint_fast8_t i = 0; i < JK_BMS::sBMS_ArrayNextIndex; ++i) { + tBattery10MilliAmpereSum += JK_BMS::BMSArray[i]->JKComputedData.Battery10MilliAmpere; + } + return tBattery10MilliAmpereSum; +} + +uint16_t JK_BMS::getSumOfTotalCapacityAmpereHour() { + int16_t tTotalCapacityAmpereHourSum = 0; + for (uint_fast8_t i = 0; i < JK_BMS::sBMS_ArrayNextIndex; ++i) { + tTotalCapacityAmpereHourSum += JK_BMS::BMSArray[i]->JKComputedData.TotalCapacityAmpereHour; + } + return tTotalCapacityAmpereHourSum; +} + +union BatteryAlarmFlagsUnion JK_BMS::getOredBatteryAlarmFlags(){ + union BatteryAlarmFlagsUnion tBatteryAlarmFlags; + tBatteryAlarmFlags.AlarmsAsWord = 0; + for (uint_fast8_t i = 0; i < JK_BMS::sBMS_ArrayNextIndex; ++i) { + tBatteryAlarmFlags.AlarmsAsWord |= JK_BMS::BMSArray[i]->JKComputedData.AlarmsAsWord; + } + return tBatteryAlarmFlags; +} + +union BMSStatusUnion JK_BMS::getOredBMSStatusFlags(){ + union BMSStatusUnion tBMSStatusFlags; + tBMSStatusFlags.StatusAsWord = 0; + for (uint_fast8_t i = 0; i < JK_BMS::sBMS_ArrayNextIndex; ++i) { + tBMSStatusFlags.StatusAsWord |= JK_BMS::BMSArray[i]->JKComputedData.StatusAsWord; + } + return tBMSStatusFlags; +} +#endif + // Identity function to avoid swapping if accidentally called uint8_t swap(uint8_t aByte) { return (aByte); @@ -488,7 +572,7 @@ void JK_BMS::fillJKConvertedCellInfo() { for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { tJKCellInfoReplyPointer++; // Skip Cell number - uint8_t tHighByte = *tJKCellInfoReplyPointer++; // Copy CellMillivolt + uint8_t tHighByte = *tJKCellInfoReplyPointer++; // Copy CellMillivolt tVoltage = tHighByte << 8 | *tJKCellInfoReplyPointer++; JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt = tVoltage; if (tVoltage > 0) { @@ -510,24 +594,24 @@ void JK_BMS::fillJKConvertedCellInfo() { JKConvertedCellInfo.RoundedAverageCellMillivolt = (tMillivoltSum + (tNumberOfNonNullCellInfo / 2)) / tNumberOfNonNullCellInfo; #if !defined(NO_CELL_STATISTICS) && defined(USE_SERIAL_2004_LCD) - /* - * Mark and count minimum and maximum cell voltages - */ - for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { - if (JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt == tMinimumMillivolt) { - JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween = VOLTAGE_IS_MINIMUM; - if (JKAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { - CellStatistics.CellMinimumArray[i]++; // count for statistics - } - } else if (JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt == tMaximumMillivolt) { - JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween = VOLTAGE_IS_MAXIMUM; - if (JKAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { - CellStatistics.CellMaximumArray[i]++; + /* + * Mark and count minimum and maximum cell voltages + */ + for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { + if (JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt == tMinimumMillivolt) { + JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween = VOLTAGE_IS_MINIMUM; + if (JKAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { + CellStatistics.CellMinimumArray[i]++; // count for statistics + } + } else if (JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt == tMaximumMillivolt) { + JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween = VOLTAGE_IS_MAXIMUM; + if (JKAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { + CellStatistics.CellMaximumArray[i]++; + } + } else { + JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween = VOLTAGE_IS_BETWEEN_MINIMUM_AND_MAXIMUM; } - } else { - JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween = VOLTAGE_IS_BETWEEN_MINIMUM_AND_MAXIMUM; } - } #endif #if !defined(NO_CELL_STATISTICS) @@ -606,8 +690,8 @@ void JK_BMS::fillJKConvertedCellInfo() { #endif // NO_CELL_STATISTICS #if defined(LOCAL_DEBUG) - Serial.print(tNumberOfCellInfo); - Serial.println(F(" cell voltages processed")); + Serial.print(tNumberOfCellInfo); + Serial.println(F(" cell voltages processed")); #endif // During JK-BMS startup all cell voltages are sent as zero for around 6 seconds if (tNumberOfNonNullCellInfo < tNumberOfCellInfo && !JKComputedData.BMSIsStarting) { @@ -728,6 +812,12 @@ void JK_BMS::fillJKComputedData() { (uint16_t) (CellStatistics.BalancingCount / 30) % 60); } #endif // NO_CELL_STATISTICS +#if defined(HANDLE_MULTIPLE_BMS) + JKComputedData.ChargeOvercurrentProtectionAmpere = swap(JKAllReplyPointer->ChargeOvercurrentProtectionAmpere); + JKComputedData.DischargeOvercurrentProtectionAmpere = swap(JKAllReplyPointer->DischargeOvercurrentProtectionAmpere); + JKComputedData.AlarmsAsWord = swap(JKAllReplyPointer->BatteryAlarmFlags.AlarmsAsWord); + JKComputedData.StatusAsWord = swap(JKAllReplyPointer->BMSStatus.StatusAsWord); +#endif } void JK_BMS::printJKCellInfoOverview() { @@ -762,8 +852,8 @@ void JK_BMS::printJKCellInfo() { Serial.print('='); Serial.print(JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt); #if defined(LOCAL_TRACE) - Serial.print(F("|0x")); - Serial.print(JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt, HEX); + Serial.print(F("|0x")); + Serial.print(JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt, HEX); #endif Serial.print(F(" mV, ")); if (i < 8) { @@ -876,13 +966,14 @@ void JK_BMS::printMiscellaneousInfo() { myPrintlnSwap(F("Balance Starting Cell Voltage[mV]="), tJKFAllReplyPointer->BalancingStartMillivolt); myPrintlnSwap(F("Balance Triggering Voltage Difference[mV]="), tJKFAllReplyPointer->BalancingStartDifferentialMillivolt); - Serial.println(); + myPrintlnSwap(F("Current Calibration[mA]="), tJKFAllReplyPointer->CurrentCalibrationMilliampere); myPrintlnSwap(F("Sleep Wait Time[s]="), tJKFAllReplyPointer->SleepWaitingTimeSeconds); - Serial.println(); + myPrintln(F("Dedicated Charge Switch Active="), tJKFAllReplyPointer->DedicatedChargerSwitchIsActive); myPrintln(F("Start Current Calibration State="), tJKFAllReplyPointer->StartCurrentCalibration); myPrintlnSwap(F("Battery Actual Capacity[Ah]="), tJKFAllReplyPointer->ActualBatteryCapacityAmpereHour); + Serial.println(); } @@ -899,8 +990,8 @@ void JK_BMS::detectAndPrintAlarmInfo() { /* * Do it only once per change */ - if (tJKAllReplyPointer->AlarmUnion.AlarmsAsWord != lastJKReply.AlarmUnion.AlarmsAsWord) { - if (tJKAllReplyPointer->AlarmUnion.AlarmsAsWord == NO_ALARM_WORD_CONTENT) { + if (tJKAllReplyPointer->BatteryAlarmFlags.AlarmsAsWord != lastJKReply.BatteryAlarmFlags.AlarmsAsWord) { + if (tJKAllReplyPointer->BatteryAlarmFlags.AlarmsAsWord == NO_ALARM_WORD_CONTENT) { Serial.println(F("All alarms are cleared now")); AlarmActive = false; } else { @@ -909,7 +1000,7 @@ void JK_BMS::detectAndPrintAlarmInfo() { /* * Print alarm info */ - uint16_t tAlarms = swap(tJKAllReplyPointer->AlarmUnion.AlarmsAsWord); + uint16_t tAlarms = swap(tJKAllReplyPointer->BatteryAlarmFlags.AlarmsAsWord); Serial.println(F("*** ALARM FLAGS ***")); Serial.print(sUpTimeString); // print uptime to have a timestamp for the alarm Serial.print(F(": Alarm bits=0x")); @@ -925,7 +1016,7 @@ void JK_BMS::detectAndPrintAlarmInfo() { Serial.print(tAlarmMask, BIN); Serial.print(F(" -> ")); #if defined(USE_SERIAL_2004_LCD) - AlarmIndexToShowOnLCD = i; + AlarmIndexToShowOnLCD = i; #endif Serial.println( reinterpret_cast((char*) (pgm_read_word(&JK_BMSAlarmStringsArray[i])))); @@ -961,10 +1052,10 @@ void JK_BMS::printJKStaticInfo() { #if defined(HANDLE_MULTIPLE_BMS) Serial.print(F("*** BMS ")); - Serial.print(NumberOfBMS); + Serial.print(NumberOfThisBMS); Serial.println(F(" INFO ***")); #else - Serial.println(F("*** BMS INFO ***")); + Serial.println(F("*** BMS INFO ***")); #endif printBMSInfo(); @@ -1033,8 +1124,8 @@ void JK_BMS::printJKDynamicInfo() { # endif // Print +CSV line every percent of nominal battery capacity (TotalCapacityAmpereHour) for capacity to voltage graph if (abs( - JKLastPrintedData.BatteryCapacityAccumulator10MilliAmpere - - JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere) > getOnePercentCapacityAsAccumulator10Milliampere()) { + JKLastPrintedData.BatteryCapacityAccumulator10MilliAmpere + - JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere) > getOnePercentCapacityAsAccumulator10Milliampere()) { JKLastPrintedData.BatteryCapacityAccumulator10MilliAmpere = JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere; printCSVLine('+'); } @@ -1097,10 +1188,10 @@ void JK_BMS::printJKDynamicInfo() { } #endif #if defined(ENABLE_MONITORING) && !defined(MONOTORING_PERIOD_FAST) - /* - * Print CSV caption every 10 minute - */ - Serial.println(reinterpret_cast(sCSVCaption)); + /* + * Print CSV caption every 10 minute + */ + Serial.println(reinterpret_cast(sCSVCaption)); #endif } // Print it every ten minutes @@ -1109,8 +1200,8 @@ void JK_BMS::printJKDynamicInfo() { * Print only if temperature changed more than 1 degree */ #if defined(LOCAL_DEBUG) - Serial.print(F("TokenTemperaturePowerMosFet=0x")); - Serial.println(JKAllReplyPointer->TokenTemperaturePowerMosFet, HEX); + Serial.print(F("TokenTemperaturePowerMosFet=0x")); + Serial.println(JKAllReplyPointer->TokenTemperaturePowerMosFet, HEX); #endif if (abs(JKComputedData.TemperaturePowerMosFet - JKLastPrintedData.TemperaturePowerMosFet) > 2 || abs(JKComputedData.TemperatureSensor1 - JKLastPrintedData.TemperatureSensor1) > 2 diff --git a/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino b/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino index 8d3963c..f47a3df 100644 --- a/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino +++ b/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino @@ -120,6 +120,9 @@ //#define MAX_CURRENT_MODIFICATION_LOWER_SOC_THRESHOLD_PERCENT 80 // Start SOC for linear reducing maximum current. Default 80 //#define MAX_CURRENT_MODIFICATION_MIN_CURRENT_TENTHS_OF_AMPERE 50 // Value of current at 100 % SOC. Units are 100 mA! Default 50 //#define DEBUG // This enables debug output for all files - only for development +#if defined(DEBUG) +#define NO_CELL_STATISTICS +#endif //#define STANDALONE_TEST // If activated, fixed BMS data is sent to CAN bus and displayed on LCD. #if defined(STANDALONE_TEST) //#define ALARM_TIMEOUT_TEST @@ -324,7 +327,7 @@ bool sAlarmOrTimeoutBeepActive = false; // If true, we do an error beep at e # if !defined(BEEP_TIMEOUT_SECONDS) #define BEEP_TIMEOUT_SECONDS 60L // 1 minute, Maximum is 254 seconds = 4 min 14 s # endif -uint8_t sBeepTimeoutCounter = 0; +uint32_t sFirstBeepMillis = 0; #endif #if defined(NO_BEEP_ON_ERROR) && defined(ONE_BEEP_ON_ERROR) #error NO_BEEP_ON_ERROR and ONE_BEEP_ON_ERROR are both defined, which makes no sense! @@ -375,9 +378,12 @@ void checkButtonPress(); JK_BMS JK_BMS_1; //#define HANDLE_MULTIPLE_BMS #if defined(HANDLE_MULTIPLE_BMS) +#define NUMBER_OF_SUPPORTED_BMS 2 JK_BMS JK_BMS_2; +JK_BMS *sCurrentBMS = &JK_BMS_1; +#else +JK_BMS *const sCurrentBMS = &JK_BMS_1; #endif - /* * CAN stuff */ @@ -636,7 +642,7 @@ delay(4000); // To be able to connect Serial monitor after reset or power up and * Copy complete reply and computed values for change determination */ JK_BMS_1.lastJKReply.SOCPercent = JK_BMS_1.JKAllReplyPointer->SOCPercent; - JK_BMS_1.lastJKReply.AlarmUnion.AlarmsAsWord = JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmsAsWord; + JK_BMS_1.lastJKReply.BatteryAlarmFlags.AlarmsAsWord = JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmsAsWord; JK_BMS_1.lastJKReply.BMSStatus.StatusAsWord = JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusAsWord; JK_BMS_1.lastJKReply.SystemWorkingMinutes = JK_BMS_1.JKAllReplyPointer->SystemWorkingMinutes; @@ -652,11 +658,6 @@ delay(4000); // To be able to connect Serial monitor after reset or power up and #endif } -#if defined(HANDLE_MULTIPLE_BMS) -JK_BMS *sCurrentBMS = &JK_BMS_1; -#else -JK_BMS *const sCurrentBMS = &JK_BMS_1; -#endif void loop() { checkButtonPress(); @@ -666,8 +667,10 @@ void loop() { /* * Request status frame every 2 seconds and check for response + * Avoid overlapping requests */ - if (millis() - sMillisOfLastRequestedJKDataFrame >= MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) { + if (!sResponseFrameBytesAreExpected + && millis() - sMillisOfLastRequestedJKDataFrame >= MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) { sMillisOfLastRequestedJKDataFrame = millis(); // set for next check sCurrentBMS->RequestStatusFrame(sCommunicationDebugModeActivated); sResponseFrameBytesAreExpected = true; // Enable check for serial input of response @@ -722,23 +725,46 @@ void loop() { sCurrentBMS->processReceivedData(); processJK_BMSStatusFrame(); // Process the complete receiving of the status frame + /******************************************************************* + * Do this once after each complete status frame or timeout or error + *******************************************************************/ + /* + * Check for overvoltage + */ + while (isVCCTooHighSimple()) { + handleOvervoltage(); + } + +# if defined(USE_SERIAL_2004_LCD) && !defined(DISPLAY_ALWAYS_ON) + if (sSerialLCDAvailable) { + doLCDBacklightTimeoutHandling(); + } +# endif + # if defined(HANDLE_MULTIPLE_BMS) - if (sCurrentBMS->NumberOfBMS == 1) { + if (sCurrentBMS->NumberOfThisBMS != JK_BMS::sBMS_ArrayNextIndex) { TURN_BMS_STATUS_LED_OFF; - // Process 2. BMS - delay(MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS / 2); // a hack - sCurrentBMS = &JK_BMS_2; + delay(200); // a hack + /* + * Switch to next BMS + */ + sCurrentBMS = JK_BMS::BMSArray[sCurrentBMS->NumberOfThisBMS]; sCurrentBMS->RequestStatusFrame(sCommunicationDebugModeActivated); sResponseFrameBytesAreExpected = true; // Enable check for serial input of response } else { /* - * Fill CAN data after both BMS are processed + * Fill CAN data after all BMS are processed + * First BMS is determines the CAN data except current, capacity and flags, which are added / OR'ed. + * and switch back to first BMS */ - fillAllCANData(sCurrentBMS); + fillAllCANData(JK_BMS::BMSArray[0]); sCANDataIsInitialized = true; // One time flag - sCurrentBMS = &JK_BMS_1; // switch back to first BMS +# if !defined(NO_BEEP_ON_ERROR) // Beep enabled + handleBeep(); // Alarm / beep handling one per period +# endif + sCurrentBMS = JK_BMS::BMSArray[0]; // switch back to first BMS } # else /* @@ -748,31 +774,14 @@ void loop() { sCANDataIsInitialized = true; // One time flag processJK_BMSStatusFrame();// Process the complete receiving of the status frame +# if !defined(NO_BEEP_ON_ERROR) // Beep enabled + handleBeep(); // Alarm / beep handling +# endif # endif TURN_BMS_STATUS_LED_OFF; sCommunicationDebugModeActivated = false; // Reset flag here. } - - /******************************************************************* - * Do this once after each complete status frame or timeout or error - *******************************************************************/ - /* - * Check for overvoltage - */ - while (isVCCTooHighSimple()) { - handleOvervoltage(); - } - -# if defined(USE_SERIAL_2004_LCD) && !defined(DISPLAY_ALWAYS_ON) - if (sSerialLCDAvailable) { - doLCDBacklightTimeoutHandling(); - } -# endif - -# if !defined(NO_BEEP_ON_ERROR) // Beep enabled - handleBeep(); // Alarm / beep handling -# endif } } #endif // defined(STANDALONE_TEST) @@ -807,16 +816,27 @@ void loop() { void handleBeep() { bool tDoBeep = false; + bool tAnyAlarmActive; + bool tAnyTimeoutActive; +#if defined(HANDLE_MULTIPLE_BMS) + // loop through all instantiated BMS + tAnyAlarmActive = JK_BMS::getAnyAlarm(); +#else + tAnyAlarmActive = sCurrentBMS->AlarmActive; + tAnyTimeoutActive = sCurrentBMS->JKBMSFrameHasTimeout; +#endif #if defined(SUPPRESS_CONSECUTIVE_SAME_ALARMS) - if (!sCurrentBMS->AlarmActive) { + if (!tAnyAlarmActive) { /* * Check for 1 hour of no alarm, then reset suppression of same alarm */ sNoConsecutiveAlarmTimeoutCounter++; // integer overflow does not really matter here - if (sNoConsecutiveAlarmTimeoutCounter - == (SUPPRESS_CONSECUTIVE_SAME_ALARMS_TIMEOUT_SECONDS * MILLIS_IN_ONE_SECOND) - / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) { + if (sNoConsecutiveAlarmTimeoutCounter == (SUPPRESS_CONSECUTIVE_SAME_ALARMS_TIMEOUT_SECONDS * MILLIS_IN_ONE_SECOND) / ( +#if defined(HANDLE_MULTIPLE_BMS) + NUMBER_OF_SUPPORTED_BMS * +#endif + MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS)) { sLastActiveAlarmsAsWord = NO_ALARM_WORD_CONTENT; // Reset LastActiveAlarmsAsWord } } @@ -824,7 +844,7 @@ void handleBeep() { if (sCurrentBMS->AlarmJustGetsActive) { sNoConsecutiveAlarmTimeoutCounter = 0; // initialize timeout counter // If new alarm is equal old one, beep only once - if (sLastActiveAlarmsAsWord == sCurrentBMS->JKAllReplyPointer->AlarmUnion.AlarmsAsWord) { + if (sLastActiveAlarmsAsWord == sCurrentBMS->JKAllReplyPointer->BatteryAlarmFlags.AlarmsAsWord) { // do only one beep for recurring alarms sCurrentBMS->AlarmJustGetsActive = false; // avoid evaluation below tDoBeep = true; // Beep only once in this loop @@ -832,7 +852,7 @@ void handleBeep() { /* * Here at least one alarm is active and it is NOT the last alarm processed */ - sLastActiveAlarmsAsWord = sCurrentBMS->JKAllReplyPointer->AlarmUnion.AlarmsAsWord; + sLastActiveAlarmsAsWord = sCurrentBMS->JKAllReplyPointer->BatteryAlarmFlags.AlarmsAsWord; } } #endif @@ -848,10 +868,16 @@ void handleBeep() { sAlarmOrTimeoutBeepActive = true; // enable beep until beep timeout #endif #if defined(MULTIPLE_BEEPS_WITH_TIMEOUT) - sBeepTimeoutCounter = 0; + sFirstBeepMillis = millis(); #endif } - if (!(sCurrentBMS->AlarmActive || sCurrentBMS->JKBMSFrameHasTimeout)) { + +#if defined(HANDLE_MULTIPLE_BMS) + tAnyTimeoutActive = JK_BMS::getAnyTimeout(); +#else + tAnyTimeoutActive = sCurrentBMS->JKBMSFrameHasTimeout; +#endif + if (!tAnyAlarmActive && !tAnyTimeoutActive) { sAlarmOrTimeoutBeepActive = false; // No more error, stop beeping } @@ -863,10 +889,10 @@ void handleBeep() { tDoBeep = true; // Beep only once in this loop } #endif + if (tDoBeep) { #if defined(MULTIPLE_BEEPS_WITH_TIMEOUT) // Beep one minute - sBeepTimeoutCounter++; // incremented at each frame request - if (sBeepTimeoutCounter == (BEEP_TIMEOUT_SECONDS * 1000U) / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) { + if ((millis() - sFirstBeepMillis) > (BEEP_TIMEOUT_SECONDS * 1000U)) { JK_INFO_PRINTLN(F("Timeout reached, suppress consecutive error beeps")); sAlarmOrTimeoutBeepActive = false; // disable further alarm beeps } else @@ -905,7 +931,7 @@ void processJK_BMSStatusFrame() { */ # if defined(HANDLE_MULTIPLE_BMS) // Print static info for both BMS - if (sCurrentBMS->NumberOfBMS == 2) { + if (sCurrentBMS->NumberOfThisBMS == 2) { sInitialActionsPerformed = true; } # else @@ -916,7 +942,7 @@ void processJK_BMSStatusFrame() { sCurrentBMS->printJKStaticInfo(); #if !defined(NO_ANALYTICS) # if defined(HANDLE_MULTIPLE_BMS) - if (sCurrentBMS->NumberOfBMS == 1) { + if (sCurrentBMS->NumberOfThisBMS == 1) { initializeAnalytics(); } # else @@ -930,7 +956,7 @@ void processJK_BMSStatusFrame() { printReceivedData(); #if !defined(NO_ANALYTICS) # if defined(HANDLE_MULTIPLE_BMS) - if (sCurrentBMS->NumberOfBMS == 1) { + if (sCurrentBMS->NumberOfThisBMS == 1) { writeSOCData(); } # else @@ -947,13 +973,13 @@ void handleFrameReceiveTimeout() { Serial.print(F("Receive timeout")); #if defined(HANDLE_MULTIPLE_BMS) Serial.print(F(" at BMS ")); - Serial.print(sCurrentBMS->NumberOfBMS); + Serial.print(sCurrentBMS->NumberOfThisBMS); #endif if (sCurrentBMS->ReplyFrameBufferIndex != 0) { // Print bytes received so far Serial.print(F(" at ReplyFrameBufferIndex=")); - Serial.print(sCurrentBMS->ReplyFrameBufferIndex); + Serial.println(sCurrentBMS->ReplyFrameBufferIndex); if (sCurrentBMS->ReplyFrameBufferIndex != 0) { sCurrentBMS->printJKReplyFrameBuffer(); } @@ -1091,7 +1117,7 @@ void initializeEEPROMForTest() { */ void testAlarmTimeout() { Serial.println(F("Test alarm timeout")); - JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm = true; + JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmBits.ChargeOvervoltageAlarm = true; JK_BMS_1.detectAndPrintAlarmInfo(); // this sets the LCD alarm string and flags Serial.println(F("Test alarm timeout. You should hear " STR(BEEP_TIMEOUT_SECONDS) " double beeps")); for (int i = 0; i < BEEP_TIMEOUT_SECONDS + 10; ++i) { @@ -1099,7 +1125,7 @@ void testAlarmTimeout() { delay(100); } // reset alarm - JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm = false; + JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmBits.ChargeOvervoltageAlarm = false; JK_BMS_1.detectAndPrintAlarmInfo(); // this sets the LCD alarm string and flags Serial.println(F("End of test alarm timeout")); diff --git a/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp b/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp index 6bafaa7..fc5d663 100644 --- a/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp +++ b/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp @@ -196,7 +196,7 @@ void printShortEnableFlagsOnLCD() { */ void printShortStateOnLCD() { - if (JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm) { + if (JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmBits.ChargeOvervoltageAlarm) { myLCD.print('O'); } else if (JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusBits.ChargeMosFetActive) { myLCD.print('C'); @@ -204,7 +204,7 @@ void printShortStateOnLCD() { myLCD.print(' '); } - if (JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm) { + if (JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmBits.DischargeUndervoltageAlarm) { myLCD.print('U'); } else if (JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusBits.DischargeMosFetActive) { myLCD.print('D'); @@ -242,13 +242,13 @@ void printLongStateOnLCD() { * or state, if no alarm or under or overvoltage alarm, which is printed also in state */ void printAlarmHexOrStateOnLCD() { - if ((JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmsAsWord + if ((JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmsAsWord & ~(MASK_OF_CHARGING_OVERVOLTAGE_ALARM_UNSWAPPED | MASK_OF_DISCHARGING_UNDERVOLTAGE_ALARM_UNSWAPPED)) == 0) { myLCD.setCursor(17, 3); // Last 3 characters are the actual states printShortStateOnLCD(); } else { myLCD.setCursor(16, 3); // Last 4 characters are the actual HEX alarm bits - uint16_t tAlarms = swap(JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmsAsWord); + uint16_t tAlarms = swap(JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmsAsWord); if (tAlarms < 0x100) { myLCD.print(F("0x")); if (tAlarms < 0x10) { @@ -697,7 +697,11 @@ void printVoltageDifferenceAndTemperature() { void printTimeoutMessageOnLCD() { myLCD.clear(); myLCD.setCursor(0, 0); - myLCD.print(F("Receive timeout ")); + myLCD.print(F("Receive timeout ")); // 16 char +#if defined(HANDLE_MULTIPLE_BMS) + myLCD.print(F("at ")); + myLCD.print(sCurrentBMS->NumberOfThisBMS); // 4 char +#endif myLCD.setCursor(LCD_COLUMNS - LENGTH_OF_UPTIME_STRING, 1); myLCD.print(sUpTimeString); myLCD.setCursor(0, 2); @@ -820,7 +824,7 @@ void printBMSDataOnLCD() { */ if (JK_BMS_1.AlarmJustGetsActive) { # if !defined(ENABLE_OVER_AND_UNDER_VOLTAGE_WARNING_ON_LCD) - if (JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmsAsWord + if (JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmsAsWord & ~MASK_OF_CHARGING_AND_DISCHARGING_OVERVOLTAGE_ALARM_UNSWAPPED) { // Other than over / undervoltage alarm bit is active # endif @@ -1061,27 +1065,27 @@ void testLCDPages() { /* * Test alarms */ - JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm = true; + JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmBits.ChargeOvervoltageAlarm = true; JK_BMS_1.detectAndPrintAlarmInfo(); // this sets the LCD alarm string printBMSDataOnLCD(); - JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm = false; + JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmBits.ChargeOvervoltageAlarm = false; delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); - JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm = true; + JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmBits.DischargeUndervoltageAlarm = true; JK_BMS_1.detectAndPrintAlarmInfo(); // this sets the LCD alarm string printBMSDataOnLCD(); - JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm = false; + JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmBits.DischargeUndervoltageAlarm = false; delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); /* * PowerMosFetOvertemperatureAlarm */ - JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm = true; + JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmBits.PowerMosFetOvertemperatureAlarm = true; JK_BMS_1.JKComputedData.TemperaturePowerMosFet = 90; JK_BMS_1.JKComputedData.TemperatureSensor1 = 25; JK_BMS_1.detectAndPrintAlarmInfo(); // this sets the LCD alarm string printBMSDataOnLCD(); - JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm = false; + JK_BMS_1.JKAllReplyPointer->BatteryAlarmFlags.AlarmBits.PowerMosFetOvertemperatureAlarm = false; JK_BMS_1.JKComputedData.TemperaturePowerMosFet = 33; delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); diff --git a/JK-BMSToPylontechCAN/Pylontech_CAN.h b/JK-BMSToPylontechCAN/Pylontech_CAN.h index 0e8f9a0..2e0dc14 100644 --- a/JK-BMSToPylontechCAN/Pylontech_CAN.h +++ b/JK-BMSToPylontechCAN/Pylontech_CAN.h @@ -2,6 +2,8 @@ * Pylontech_CAN.h * * Definitions for the CAN frames to send as Pylon protocol. + * For multiple BMS current and capacity are added, flags are OR'ed. + * * TODO The generated output does not correspond to the logs below and * published at https://www.setfirelabs.com/green-energy/pylontech-can-reading-can-replication * @@ -109,11 +111,21 @@ struct PylontechCANBatteryLimitsFrame351Struct { } FrameData; void fillFrame(JK_BMS *aJK_BMS_Ptr) { FrameData.BatteryChargeOvervoltage100Millivolt = aJK_BMS_Ptr->JKComputedData.BatteryFullVoltage10Millivolt / 10; - FrameData.BatteryChargeCurrentLimit100Milliampere = swap(aJK_BMS_Ptr->JKAllReplyPointer->ChargeOvercurrentProtectionAmpere) - * 10; - FrameData.BatteryDischargeCurrentLimit100Milliampere = swap( - aJK_BMS_Ptr->JKAllReplyPointer->DischargeOvercurrentProtectionAmpere) * 10; + FrameData.BatteryDischarge100Millivolt = aJK_BMS_Ptr->JKComputedData.BatteryEmptyVoltage10Millivolt / 10; +#if defined(HANDLE_MULTIPLE_BMS) + /* + * Use sum of current limits + */ + FrameData.BatteryChargeCurrentLimit100Milliampere = JK_BMS::getSumOfChargeOvercurrentProtectionAmpere() * 10; + FrameData.BatteryDischargeCurrentLimit100Milliampere = JK_BMS::getSumOfDischargeOvercurrentProtectionAmpere() * 10; +#else + FrameData.BatteryChargeCurrentLimit100Milliampere = swap(aJK_BMS_Ptr->JKAllReplyPointer->ChargeOvercurrentProtectionAmpere) + * 10; + FrameData.BatteryDischargeCurrentLimit100Milliampere = swap( + aJK_BMS_Ptr->JKAllReplyPointer->DischargeOvercurrentProtectionAmpere) * 10; +#endif + } }; @@ -147,7 +159,11 @@ struct PylontechCANCurrentValuesFrame356Struct { } FrameData; void fillFrame(JK_BMS *aJK_BMS_Ptr) { FrameData.Voltage10Millivolt = aJK_BMS_Ptr->JKComputedData.BatteryVoltage10Millivolt; +#if defined(HANDLE_MULTIPLE_BMS) + FrameData.Current100Milliampere = JK_BMS::getSumOfBattery10MilliAmpere() / 10; +#else FrameData.Current100Milliampere = aJK_BMS_Ptr->JKComputedData.Battery10MilliAmpere / 10; +#endif FrameData.Temperature100Millicelsius = aJK_BMS_Ptr->JKComputedData.TemperatureMaximum * 10; } }; @@ -204,40 +220,44 @@ struct PylontechCANErrorsWarningsFrame359Struct { uint8_t Token2 = 0x4E; // 'N' } FrameData; void fillFrame(JK_BMS *aJK_BMS_Ptr) { - struct JKReplyStruct *tJKFAllReply = aJK_BMS_Ptr->JKAllReplyPointer; +#if defined(HANDLE_MULTIPLE_BMS) + union BatteryAlarmFlagsUnion tBatteryAlarmFlags = JK_BMS::getOredBatteryAlarmFlags(); +#else + union BatteryAlarmFlagsUnion tBatteryAlarmFlags = aJK_BMS_Ptr->JKAllReplyPointer->BatteryAlarmFlags; +#endif /* * Pylon has no battery over voltage alarm but cell over voltage warning and error * We (mis)use the battery alarms as cell warnings */ // Byte 0 - FrameData.CellOvervoltageError = tJKFAllReply->AlarmUnion.AlarmBits.CellOvervoltageAlarm; - FrameData.CellUndervoltageError = tJKFAllReply->AlarmUnion.AlarmBits.CellUndervoltageAlarm; - FrameData.CellOvertemperatureError = tJKFAllReply->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm - || tJKFAllReply->AlarmUnion.AlarmBits.Sensor1Or2OvertemperatureAlarm; - FrameData.CellUndertemperatureError = tJKFAllReply->AlarmUnion.AlarmBits.Sensor1Or2UndertemperatureAlarm; - FrameData.DischargeOvercurrentError = tJKFAllReply->AlarmUnion.AlarmBits.DischargeOvercurrentAlarm; + FrameData.CellOvervoltageError = tBatteryAlarmFlags.AlarmBits.CellOvervoltageAlarm; + FrameData.CellUndervoltageError = tBatteryAlarmFlags.AlarmBits.CellUndervoltageAlarm; + FrameData.CellOvertemperatureError = tBatteryAlarmFlags.AlarmBits.PowerMosFetOvertemperatureAlarm + || tBatteryAlarmFlags.AlarmBits.Sensor1Or2OvertemperatureAlarm; + FrameData.CellUndertemperatureError = tBatteryAlarmFlags.AlarmBits.Sensor1Or2UndertemperatureAlarm; + FrameData.DischargeOvercurrentError = tBatteryAlarmFlags.AlarmBits.DischargeOvercurrentAlarm; // Byte 1 - FrameData.ChargeOvercurrentError = tJKFAllReply->AlarmUnion.AlarmBits.ChargeOvercurrentAlarm; - FrameData.SystemError = tJKFAllReply->BMSStatus.StatusBits.BatteryDown; + FrameData.ChargeOvercurrentError = tBatteryAlarmFlags.AlarmBits.ChargeOvercurrentAlarm; + FrameData.SystemError = aJK_BMS_Ptr->JKAllReplyPointer->BMSStatus.StatusBits.BatteryDown; // if (tJKFAllReply->SOCPercent < 5) { // FrameData.SystemError = 1; // } // Byte 2 // (mis)use the battery alarms as cell warnings for Pylon - FrameData.CellHighVoltageWarning = tJKFAllReply->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm; - FrameData.CellLowVoltageWarning = tJKFAllReply->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm; + FrameData.CellHighVoltageWarning = tBatteryAlarmFlags.AlarmBits.ChargeOvervoltageAlarm; + FrameData.CellLowVoltageWarning = tBatteryAlarmFlags.AlarmBits.DischargeUndervoltageAlarm; // Use the same values as for error here - FrameData.CellHighTemperatureWarning = tJKFAllReply->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm - || tJKFAllReply->AlarmUnion.AlarmBits.Sensor1Or2OvertemperatureAlarm; - FrameData.CellLowTemperatureWarning = tJKFAllReply->AlarmUnion.AlarmBits.Sensor1Or2UndertemperatureAlarm; - FrameData.DischargeHighCurrentWarning = tJKFAllReply->AlarmUnion.AlarmBits.DischargeOvercurrentAlarm; + FrameData.CellHighTemperatureWarning = tBatteryAlarmFlags.AlarmBits.PowerMosFetOvertemperatureAlarm + || tBatteryAlarmFlags.AlarmBits.Sensor1Or2OvertemperatureAlarm; + FrameData.CellLowTemperatureWarning = tBatteryAlarmFlags.AlarmBits.Sensor1Or2UndertemperatureAlarm; + FrameData.DischargeHighCurrentWarning = tBatteryAlarmFlags.AlarmBits.DischargeOvercurrentAlarm; // Byte 3 // Use the same values as for error here - FrameData.ChargeHighCurrentWarning = tJKFAllReply->AlarmUnion.AlarmBits.ChargeOvercurrentAlarm; - FrameData.SystemError = tJKFAllReply->BMSStatus.StatusBits.BatteryDown; + FrameData.ChargeHighCurrentWarning = tBatteryAlarmFlags.AlarmBits.ChargeOvercurrentAlarm; + FrameData.SystemError = aJK_BMS_Ptr->JKAllReplyPointer->BMSStatus.StatusBits.BatteryDown; // if (tJKFAllReply->SOCPercent < 10) { // FrameData.SystemWarning = 1; @@ -292,8 +312,15 @@ struct PylontechCANBatteryRequesFrame35CStruct { } else { FrameData.ForceChargeRequestII = 0; } + +#if defined(HANDLE_MULTIPLE_BMS) + union BMSStatusUnion tBMSStatusFlags = JK_BMS::getOredBMSStatusFlags(); + FrameData.DischargeEnable = tBMSStatusFlags.StatusBits.ChargeMosFetActive; + FrameData.ChargeEnable = tBMSStatusFlags.StatusBits.DischargeMosFetActive; +#else FrameData.DischargeEnable = tJKFAllReply->BMSStatus.StatusBits.ChargeMosFetActive; FrameData.ChargeEnable = tJKFAllReply->BMSStatus.StatusBits.DischargeMosFetActive; +#endif } }; @@ -330,8 +357,12 @@ struct PylontechCANSMACapacityFrame35FStruct { struct JKReplyStruct *tJKFAllReply = aJK_BMS_Ptr->JKAllReplyPointer; FrameData.SoftwareVersionLowByte = tJKFAllReply->SoftwareVersionNumber[1]; FrameData.SoftwareVersionHighByte = tJKFAllReply->SoftwareVersionNumber[0]; +#if defined(HANDLE_MULTIPLE_BMS) + FrameData.CapacityAmpereHour = JK_BMS::getSumOfTotalCapacityAmpereHour(); +#else FrameData.CapacityAmpereHour = aJK_BMS_Ptr->JKComputedData.TotalCapacityAmpereHour; - } +#endif +} }; /* diff --git a/README.md b/README.md index ba34a6b..5a5f0e0 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ The JK-BMS RS485 data (e.g. at connector GPS) are provided as RS232 TTL with 115 - Protocol converter from the JK-BMS status frame to Pylontech CAN frames. - Supports sending of total capayity for **SMA** and **Luxpower** inverters. - Optional linear **reducing maximum current above 80% SOC** (values can be adapted to your needs). -- Support for more than one BMS (experimental). +- Support for **multiple BMS**. - Display of BMS information, Cell voltages, statistics and alarms on a locally attached **serial 2004 LCD**. - Page button for switching **5 LCD display pages**. - Debug output and extra **CAN info** and **Capacity info page** on long press of button. @@ -115,8 +115,10 @@ The same (raw) data without ESR correction of voltage.
# Youtube video of JK-BMS doing wrong computing of capacity. -I discharged the battery for 10 minutes with 45A, which gives 7.5Ah. But the JK-BMS shows a capacity loss of 12.1 Ah (50.7 - 38.6)! -The SOC went from 40 % to 30 %, which corresponds also to 12.5 Ah for a total battery capacity of 125 Ah. +I discharged the battery for 10 minutes with 45A, which gives **7.5 Ah** capacity loss.
+Start of 10 minutes period is left screenshot with time of ...**21M40S** and Capacity **50.7 Ah**, end of period is right screenshot with time of ...**31M40S** and Capacity **38.6 Ah**.
+These 2 values correspond to a **capacity loss of 12.1 Ah** (50.7 Ah - 38.6 Ah)!
+SOC went from 40 % to 30 %, which at least corresponds to 12.5 Ah for a total battery capacity of 125 Ah. [![Youtube video of JK-BMS doing wrong computing of capacity](https://i.ytimg.com/vi/tDN8iFr98JA/hqdefault.jpg)](https://www.youtube.com/watch?v=tDN8iFr98JA) @@ -178,11 +180,30 @@ Depending on the USB chip, the pull down can be down to 680 Ohm. |GND RX TX VBAT| |________________| | | | - | | --|<|-- RX of Uno / Nano - | ----------- D4 (or other pin, if specified differently) - --------------- GND + | | o-|<|-- RX of Uno / Nano + | o---------- D4 (or other pin, if specified differently) + o-------------- GND +# Using an abitrary PNP transistor instead of a diode between BMS TX and Nano RX, +# to support weak TX outputs and multiple BMS connected to one Nano RX. + ___ ________ ___ + | | + | O O O O | + |GND RX TX VBAT| + |________________| + | | | + | | | o GND + | | | | + | | | |/ C + | | o----| Any PNP will work! + | | B |< + | | | E + | | o-- RX of Uno / Nano + | o------------- D4 (or other pin, if specified differently) + o----------------- GND + + # Automatic brightness control for 2004 LCD 5V O------o------o | | @@ -194,7 +215,7 @@ Depending on the USB chip, the pull down can be down to 680 Ohm. o----| |> | - O To anode of LCD backlight + o To anode of LCD backlight Alternative circuit for VCC lower than 5 volt e.g. for supply by Li-ion battery