diff --git a/JK-BMSToPylontechCAN/AVRUtils.cpp b/JK-BMSToPylontechCAN/AVRUtils.cpp index 2e00b03..187bde2 100644 --- a/JK-BMSToPylontechCAN/AVRUtils.cpp +++ b/JK-BMSToPylontechCAN/AVRUtils.cpp @@ -30,219 +30,307 @@ #include #include #include // for __malloc_margin -/* - * The largest address just not allocated so far - * Under Unix, the "break value" was the end of the data - * segment as dynamically requested from the operating system. - * Since we don't have an operating system, just make sure - * that we don't collide with the stack. - */ -extern void *__brkval; // The largest address just not allocated so far /* - * Returns actual start of free heap + * Returns actual start of available / free heap * Usage for print: - Serial.print(F("HeapStart=0x")); - Serial.println((uintptr_t) getHeapStart(), HEX); + Serial.print(F("AvailableHeapStart=0x")); + Serial.println((uint16_t) getAvailableHeapStart(), HEX); */ -uint8_t* getHeapStart(void) { +uint8_t* getAvailableHeapStart(void) { if (__brkval == 0) { - __brkval = __malloc_heap_start; + // __brkval is 0 if no malloc() has happened before +// __brkval = __malloc_heap_start; + __brkval = &__heap_start; } return (uint8_t*) __brkval; } +void printAvailableHeapStart(Print *aSerial) { + aSerial->print(F("Heap start=")); + aSerial->println((uint16_t) getAvailableHeapStart()); +} /* * Initialize RAM between current stack and actual heap start (__brkval) with pattern 0x5A */ void initStackFreeMeasurement() { - uint8_t tDummyVariableOnStack; - uint8_t *tHeapPtr = getHeapStart(); + uint8_t *tHeapPtr = getAvailableHeapStart(); // This sets __brkval // Fill / paint stack do { *tHeapPtr++ = HEAP_STACK_UNTOUCHED_VALUE; - } while (tHeapPtr < &tDummyVariableOnStack); + } while (tHeapPtr < (uint8_t*) SP); } /* - * Returns the amount of stack/heap not used/touched since the last call to initStackFreeMeasurement() - * by check for first touched pattern on the stack/heap, starting the UPWARD search at heap start. - * Sets the variable aStackUsedBytesPointer points to with amount of used/touched bytes. - * - * This fails, if a HEAP_STACK_UNTOUCHED_VALUE was written in a former malloced block. + * @return The amount of used/touched bytes since the last call to initStackFreeMeasurement() + * -1 if stack was completely used + * Check for first touched pattern on the stack/heap, starting the UPWARD search at heap start. */ -uint16_t getStackUnusedAndUsedBytes(uint16_t *aStackUsedBytesPointer) { - uint8_t tDummyVariableOnStack; - uint8_t *tHeapPtr = getHeapStart(); - -// first search for first untouched value after current begin of heap, because malloc() and free() may be happened in between and overwrite low memory - while (*tHeapPtr != HEAP_STACK_UNTOUCHED_VALUE && tHeapPtr < &tDummyVariableOnStack) { - tHeapPtr++; +/* + * @param aStackUnusedSizePointer points to variable which is written with amount of stack/heap not used/touched. + * @return The amount of stack/heap touched since the last call to initStackFreeMeasurement() + * -1 if stack was completely used + * Do a downward search, because upward may be wrong, because malloc does not initialize the memory + * and the search fails with multiple mallocs and partial writing of allocated regions. + * Check for first two untouched pattern on the stack/heap, starting the DOWNWARD search at current stack pointer. + */ +//#include // for Serial +int16_t getStackMaxUsedAndUnusedSizes(uint16_t *aStackUnusedSizePointer) { + uint8_t *tAvailableHeapStart = getAvailableHeapStart(); // __brkval + uint8_t *tStackSearchPtr = (uint8_t*) SP; + + // Search for first two untouched values below current stackpointer. + // tStackSearchPtr > tAvailableHeapStart avoids overflow if stack was completely touched before. + while (!(*tStackSearchPtr == HEAP_STACK_UNTOUCHED_VALUE && *(tStackSearchPtr - 1) == HEAP_STACK_UNTOUCHED_VALUE) + && tStackSearchPtr > tAvailableHeapStart) { + tStackSearchPtr--; } -// then count untouched patterns +// Serial.println((uint16_t) tStackSearchPtr, HEX); +// Serial.println((uint16_t) tAvailableHeapStart, HEX); + /* + * tStackSearchPtr points now to highest untouched stack position + */ + int16_t tStackMaxUsedSize = RAMEND - (uint16_t) tStackSearchPtr; + + // Search for first touched value used stack. uint16_t tStackUnused = 0; - while (*tHeapPtr == HEAP_STACK_UNTOUCHED_VALUE && tHeapPtr < &tDummyVariableOnStack) { - tHeapPtr++; + while (*tStackSearchPtr == HEAP_STACK_UNTOUCHED_VALUE && tStackSearchPtr > tAvailableHeapStart) { + tStackSearchPtr--; tStackUnused++; } - *aStackUsedBytesPointer = ((RAMEND + 1) - (uint16_t) tHeapPtr); - - return tStackUnused; + *aStackUnusedSizePointer = tStackUnused; + if (tStackUnused == 0) { + return -1; + } + return tStackMaxUsedSize; } /* - * Returns the amount of stack/heap touched since the last call to initStackFreeMeasurement() - * by check for first non touched pattern on the stack/heap, starting the DOWNWARD search at current stack pointer. - * - * If the end of former malloced and written memory was higher than current stackpointer, - * the memory area is taken as overwritten stack. Therefore we may return values, which are too big. - * But nevertheless, this constellation is likely a potential programming problem! + * Prints the amount of stack NOT used/touched and used/touched since the last call to initStackFreeMeasurement() + * Example: "Stack unused=0, used=16" if stack runs into data */ -uint16_t getStackUsedBytes() { - uint8_t tDummyVariableOnStack; - uint8_t *tHeapStart = getHeapStart(); - uint8_t *tSearchPtr = &tDummyVariableOnStack; - - // Search for first untouched value below current stackpointer - while (*tSearchPtr != HEAP_STACK_UNTOUCHED_VALUE && tSearchPtr > tHeapStart) { - tSearchPtr--; - } - - return (RAMEND + 1) - (uint16_t) tSearchPtr; +void printStackMaxUsedAndUnusedSizes(Print *aSerial) { + uint16_t tStackUnusedBytes; + aSerial->print(F("Stack used=")); + aSerial->print(RAMEND - SP); + aSerial->print(F(", max used=")); + aSerial->print(getStackMaxUsedAndUnusedSizes(&tStackUnusedBytes)); + aSerial->print(F(", unused=")); + aSerial->print(tStackUnusedBytes); + aSerial->print(F(" of current total ")); + aSerial->println((RAMEND + 1) - (uint16_t) getAvailableHeapStart()); } /* - * Returns the amount of stack/heap not touched since the last call to initStackFreeMeasurement() - * by check for first touched pattern on the stack/heap, starting the search at heap start. + * Search upwards the first two HEAP_STACK_UNTOUCHED_VALUE values after current begin of heap */ -uint16_t getStackUnusedBytes() { - uint8_t tDummyVariableOnStack; - uint8_t *tHeapPtr = getHeapStart(); - -// first search for first match after current begin of heap, because malloc() and free() may be happened in between and overwrite low memory - while (*tHeapPtr != HEAP_STACK_UNTOUCHED_VALUE && tHeapPtr < &tDummyVariableOnStack) { +uint16_t getHeapMaxUsedSize() { + uint8_t *tHeapPtr = getAvailableHeapStart(); + while (*tHeapPtr != HEAP_STACK_UNTOUCHED_VALUE && *(tHeapPtr + 1) != HEAP_STACK_UNTOUCHED_VALUE && tHeapPtr <= (uint8_t*) SP) { tHeapPtr++; } -// then count untouched patterns - uint16_t tStackUnused = 0; - while (*tHeapPtr == HEAP_STACK_UNTOUCHED_VALUE && tHeapPtr < &tDummyVariableOnStack) { - tHeapPtr++; - tStackUnused++; + // tHeapPtr points now to lowest untouched stack position or to lowest current stack byte + return tHeapPtr - (uint8_t*) __malloc_heap_start; +} + +/* + * Prints the amount of stack NOT used/touched and used/touched since the last call to initStackFreeMeasurement() + * Print only if value changed. + * @return true, if values changed + */ +bool printStackMaxUsedAndUnusedSizesIfChanged(Print *aSerial) { + static int16_t tOldStackUsedBytes = 0; + + uint16_t tStackUnusedBytes; + int16_t tStackMaxUsedBytes = getStackMaxUsedAndUnusedSizes(&tStackUnusedBytes); + if (tOldStackUsedBytes != tStackMaxUsedBytes) { + tOldStackUsedBytes = tStackMaxUsedBytes; + aSerial->print(F("Stack used=")); + aSerial->print(RAMEND - SP); + aSerial->print(F(", max used=")); + aSerial->print(tStackMaxUsedBytes); + aSerial->print(F(", unused=")); + aSerial->println(tStackUnusedBytes); + return true; } - return tStackUnused; + return false; } /* - * Get amount of free RAM = current stackpointer - heap end + * Get amount of free Stack = current stackpointer - heap end */ -uint16_t getCurrentFreeHeapOrStack(void) { - uint16_t tHeapStart = (uint16_t) getHeapStart(); - if (tHeapStart >= SP) { +uint16_t getCurrentAvailableStackSize(void) { + uint16_t tAvailableHeapStart = (uint16_t) getAvailableHeapStart(); // __brkval + if (tAvailableHeapStart >= SP) { return 0; } - return (SP - (uint16_t) getHeapStart()); + return (SP - tAvailableHeapStart); +} +void printCurrentAvailableStackSize(Print *aSerial) { + aSerial->print(F("Currently available Stack[bytes]=")); + aSerial->println(getCurrentAvailableStackSize()); } /* * Get amount of maximum available memory for malloc() * FreeRam - __malloc_margin (128 for ATmega328) */ -uint16_t getCurrentAvailableHeap(void) { - if (getCurrentFreeHeapOrStack() <= __malloc_margin) { +uint16_t getCurrentAvailableHeapSize(void) { + if (getCurrentAvailableStackSize() <= (__malloc_margin + HEURISTIC_ADDITIONAL_MALLOC_MARGIN)) { return 0; } - return getCurrentFreeHeapOrStack() - __malloc_margin; // (128) + // SP - __brkval - (__malloc_margin + HEURISTIC_ADDITIONAL_MALLOC_MARGIN) + return getCurrentAvailableStackSize() - (__malloc_margin + HEURISTIC_ADDITIONAL_MALLOC_MARGIN); // (128) } -void printCurrentAvailableHeap(Print *aSerial) { - aSerial->print(F("Currently max available Heap[bytes]=")); - aSerial->println(getCurrentAvailableHeap()); -} - -void printHeapStart(Print *aSerial) { - aSerial->print(F("Heap start=")); - aSerial->println((uint16_t) getHeapStart()); +void printCurrentAvailableHeapSize(Print *aSerial) { + aSerial->print(F("Currently available Heap[bytes]=")); + aSerial->println(getCurrentAvailableHeapSize()); } /* - * Prints the amount of stack used/touched since the last call to initStackFreeMeasurement() - * Example: "Stack used 20 of 7" up to "Stack used 20 of 20" if stack runs into data + * Simple and short implementation, does not work before initStackFreeMeasurement() or first malloc() + * The STACK required for this function is 4 bytes, so available numbers are 4 less than for caller. */ -void printStackUsedBytes(Print *aSerial) { - aSerial->print(F("Stack used ")); - aSerial->print(getStackUsedBytes()); - aSerial->print(F(" of ")); - aSerial->println((RAMEND + 1) - (uint16_t) getHeapStart()); +void printCurrentAvailableHeapSizeSimple(Print *aSerial) { + aSerial->print(F("available=")); + aSerial->println(SP - (uint16_t) __brkval + 1 - ((uint16_t) __malloc_margin + HEURISTIC_ADDITIONAL_MALLOC_MARGIN)); } -/* - * Prints the amount of stack NOT used/touched and used/touched since the last call to initStackFreeMeasurement() - * Example: "Stack unused=0, used=16" if stack runs into data - */ -void printStackUnusedAndUsedBytes(Print *aSerial) { - uint16_t tStackUsedBytes; - aSerial->print(F("Stack unused=")); - aSerial->print(getStackUnusedAndUsedBytes(&tStackUsedBytes)); - aSerial->print(F(", used=")); - aSerial->println(tStackUsedBytes); -} +// This define is in AVRUtils.h +// #define PRINT_AVAILABLE_HEAP Serial.print(F("available="));Serial.println(SP - (uint16_t) __brkval + 1 - ((uint16_t) __malloc_margin + HEURISTIC_ADDITIONAL_MALLOC_MARGIN)) -/* - * Prints the amount of stack NOT used/touched and used/touched since the last call to initStackFreeMeasurement() - * Print only if value changed. - */ -void printStackUnusedAndUsedBytesIfChanged(Print *aSerial) { - static uint16_t sStackUsedBytes = 0; - - uint16_t tOldStackUsedBytes = sStackUsedBytes; - uint16_t tStackUnusedBytes = getStackUnusedAndUsedBytes(&sStackUsedBytes); - if (tOldStackUsedBytes != sStackUsedBytes) { - aSerial->print(F("Stack unused=")); - aSerial->print(tStackUnusedBytes); - aSerial->print(F(", used=")); - aSerial->println(sStackUsedBytes); - } -} +void printBaseRAMData(Print *aSerial) { + aSerial->print(F("__malloc_heap_start=")); + aSerial->print((uint16_t) __malloc_heap_start); // = __bss_end, __heap_start in lst file + aSerial->print(F("|0x")); + aSerial->print((uint16_t) __malloc_heap_start, HEX); + + aSerial->print(F(", &__heap_start=")); + aSerial->print((uint16_t) &__heap_start); + aSerial->print(F("|0x")); + aSerial->print((uint16_t) &__heap_start, HEX); + aSerial->print(F(", __brkval=")); + aSerial->print((uint16_t) __brkval); // The largest address just not allocated so far / start of available / free heap, initialized at first malloc() + aSerial->print(F("|0x")); + aSerial->print((uint16_t) __brkval, HEX); + + aSerial->print(F(", __malloc_margin=")); + aSerial->print((uint16_t) __malloc_margin); // =128 + + aSerial->print(F(", SP=")); + aSerial->print((uint16_t) SP); + aSerial->print(F("|0x")); + aSerial->print((uint16_t) SP, HEX); + + /* + * The next 2 entries seems to be always 0 + */ + aSerial->print(F(", __malloc_heap_end=")); + aSerial->print((uint16_t) __malloc_heap_end); + + aSerial->print(F(", __flp=")); + aSerial->print((uint16_t) __flp); // The largest address just not allocated so far / start of available / free heap, initialized at first malloc() + aSerial->println(); +} /* - * RAM starts of variables initialized with values != 0, - * followed by variables initialized with 0 + * RAM starts with Data, i.e. variables initialized with values != 0, + * followed by BSS, i.e. uninitalized variables (which are initialized with 0) * and variables not initialized by using attribute "__attribute__((section(".noinit")))". * It ends with the heap and the stack. * - * Sample output if stack runs into data: - * Size of Data + BSS / Heap start=0x773 | 1907 - * Currently available Heap=7 - * Stack used 100 of 141 + * The STACK required for this function is 8 bytes, so available numbers are 8 less than for caller. + * + * Sample output: + * Data+BSS=445. Heap: used=770, max used=1096, available=663. Stack: available=791, used=42, max used=319, unused=188 of current total 833 + * Formulas: + * Stack available + used = current total + * Heap available + __malloc_margin (128) = Stack available + * Data+BSS + Heap max used + Stack unused + Stack max used = RAMSIZE */ void printRAMInfo(Print *aSerial) { - uint16_t tHeapStart = (uint16_t) getHeapStart(); - aSerial->print(F("Size of Data + BSS / Heap start=0x")); - aSerial->print(tHeapStart - RAMSTART, HEX); - aSerial->print(F(" | ")); - aSerial->println(tHeapStart - RAMSTART); - aSerial->print(F("Currently available Heap=")); - aSerial->println(getCurrentAvailableHeap()); + aSerial->print(F("Data+BSS=")); + aSerial->print((uint16_t) &__heap_start - RAMSTART); - printStackUsedBytes(aSerial); + aSerial->print(F(". Heap: used=")); + aSerial->print((uint16_t) getAvailableHeapStart() - (uint16_t) &__heap_start); + aSerial->print(F(", max used=")); + aSerial->print(getHeapMaxUsedSize()); + aSerial->print(F(", available=")); + uint16_t tStackAvailable = SP - (uint16_t) getAvailableHeapStart() + 1; + aSerial->print(tStackAvailable - (uint16_t) __malloc_margin + HEURISTIC_ADDITIONAL_MALLOC_MARGIN); + aSerial->print(F(". Stack: available=")); + aSerial->print(tStackAvailable); + aSerial->print(F(", used=")); + aSerial->print(RAMEND - SP); + uint16_t tStackUnusedBytes; + aSerial->print(F(", max used=")); + aSerial->print(getStackMaxUsedAndUnusedSizes(&tStackUnusedBytes)); + aSerial->print(F(", unused=")); + aSerial->print(tStackUnusedBytes); + aSerial->print(F(" of current total ")); + aSerial->print((RAMEND + 1) - (uint16_t) getAvailableHeapStart()); // getAvailableHeapStart() + + aSerial->println(); +} + +void set__malloc_margin(uint8_t aNewMallocMargin) { + __malloc_margin = aNewMallocMargin; } -void printCurrentFreeHeap(Print *aSerial) { - aSerial->print(F("Current free Heap / Stack[bytes]=")); - aSerial->println(getCurrentFreeHeapOrStack()); +void reset__malloc_margin() { + __malloc_margin = 128; } bool isAddressInRAM(void *aAddressToCheck) { return (aAddressToCheck <= (void*) RAMEND); } -bool isAddressBelowHeap(void *aAddressToCheck) { - return (aAddressToCheck < getHeapStart()); +bool isAddressBelowAvailableHeapStart(void *aAddressToCheck) { + return (aAddressToCheck < getAvailableHeapStart()); } +/* + * Test available heap by callocing 128 bytes chunks, + * If no memory available, try with 64, 32 etc up to 2, 1 byte chunks + */ +void testCallocSizesAndPrint(Print *aSerial) { + uint8_t *tLastMallocPtr; + uint16_t tMallocSize = 128; + while (true) { + aSerial->print(F("SP=0x")); + aSerial->print(SP, HEX); + aSerial->print(F(" available=")); + aSerial->print(SP - (uint16_t) __brkval + 1 - ((uint16_t) __malloc_margin + HEURISTIC_ADDITIONAL_MALLOC_MARGIN)); + uint8_t *tMallocPtr = (uint8_t*) calloc(tMallocSize, 1); + + aSerial->print(F(" -> calloc(")); + aSerial->print(tMallocSize); + aSerial->print(F(",1)")); + + if (tMallocPtr == NULL) { + aSerial->print(F("failed ->")); + tMallocSize = tMallocSize >> 1; + if (tMallocSize < 1) { + aSerial->println(); + break; + } + } else { + tLastMallocPtr = tMallocPtr; + aSerial->print(F("=0x")); + aSerial->print((uint16_t) tLastMallocPtr, HEX); + aSerial->print(F(" ->")); + + *tLastMallocPtr = HEAP_STACK_UNTOUCHED_VALUE; // For testing detection using 2 consecutive HEAP_STACK_UNTOUCHED_VALUE + *(tLastMallocPtr + tMallocSize - 1) = 0x11; + } + printCurrentAvailableHeapSizeSimple(aSerial); + } +} /******************************************** * SLEEP AND WATCHDOG STUFF ********************************************/ diff --git a/JK-BMSToPylontechCAN/AVRUtils.h b/JK-BMSToPylontechCAN/AVRUtils.h index 7ed790b..32a9bf5 100644 --- a/JK-BMSToPylontechCAN/AVRUtils.h +++ b/JK-BMSToPylontechCAN/AVRUtils.h @@ -1,7 +1,7 @@ /* * AVRUtils.h * - * Copyright (C) 2016-2020 Armin Joachimsmeyer + * Copyright (C) 2016-2024 Armin Joachimsmeyer * Email: armin.joachimsmeyer@gmail.com * * This file is part of Arduino-Utils https://github.com/ArminJo/Arduino-Utils. @@ -29,6 +29,18 @@ #include #include "avr/boot.h" +/* + * The largest address just not allocated so far + * Under Unix, the "break value" was the end of the data + * segment as dynamically requested from the operating system. + * Since we don't have an operating system, just make sure + * that we don't collide with the stack. + */ +extern void *__brkval; // The largest address just not allocated so far / start of available / free heap, initialized at first malloc() +extern void *__flp; // +extern char __heap_start; // = __bss_end, the linker address of heap start +#define HEURISTIC_ADDITIONAL_MALLOC_MARGIN 14 // No malloc() possible if size is lower than (__malloc_margin + HEURISTIC_ADDITIONAL_MALLOC_MARGIN) + /* * storage for millis value to enable compensation for interrupt disable at signal acquisition etc. */ @@ -55,26 +67,32 @@ extern volatile uint16_t sNumberOfSleeps; #include -uint8_t* getHeapStart(); -uint16_t getCurrentFreeHeapOrStack(void); -uint16_t getCurrentAvailableHeap(void); -void printHeapStart(Print *aSerial); -void printCurrentFreeHeap(Print *aSerial); -void printCurrentAvailableHeap(Print *aSerial); +uint8_t* getAvailableHeapStart(); +void printAvailableHeapStart(Print *aSerial); +uint16_t getCurrentAvailableStackSize(void); +void printCurrentAvailableStackSize(Print *aSerial); +uint16_t getCurrentAvailableHeapSize(void); +void printCurrentAvailableHeapSize(Print *aSerial); +void printCurrentAvailableHeapSizeSimple(Print *aSerial); +#define PRINT_AVAILABLE_HEAP Serial.print(F("available="));Serial.println(SP - (uint16_t) __brkval + 1 - ((uint16_t) __malloc_margin + HEURISTIC_ADDITIONAL_MALLOC_MARGIN)) #define HEAP_STACK_UNTOUCHED_VALUE 0x5A void initStackFreeMeasurement(); -uint16_t getStackUnusedBytes(); -uint16_t getStackUsedBytes(); -uint16_t getStackUnusedAndUsedBytes(uint16_t *aStackUsedBytesPointer); -void printStackUsedBytes(Print *aSerial); -void printStackUnusedAndUsedBytes(Print *aSerial); -void printStackUnusedAndUsedBytesIfChanged(Print *aSerial); +int16_t getStackMaxUsedAndUnusedSizes(uint16_t *aStackUnusedSizePointer); +void printStackMaxUsedAndUnusedSizes(Print *aSerial); +bool printStackMaxUsedAndUnusedSizesIfChanged(Print *aSerial); + +void printBaseRAMData(Print *aSerial); void printRAMInfo(Print *aSerial); bool isAddressInRAM(void *aAddressToCheck); -bool isAddressBelowHeap(void *aAddressToCheck); +bool isAddressBelowAvailableHeapStart(void *aAddressToCheck); + +void set__malloc_margin(uint8_t aNewMallocMargin); +void reset__malloc_margin(); + +void testCallocSizesAndPrint(Print *aSerial); #endif // defined(__AVR__) #endif // _AVR_UTILS_H diff --git a/JK-BMSToPylontechCAN/HexDump.hpp b/JK-BMSToPylontechCAN/HexDump.hpp index f63fdf6..b3b8d45 100644 --- a/JK-BMSToPylontechCAN/HexDump.hpp +++ b/JK-BMSToPylontechCAN/HexDump.hpp @@ -108,10 +108,10 @@ void printMemoryHexDump(uint8_t *aMemory, uint16_t aNumberOfBytesToPrint, uint8_ */ Serial.print(F(" ")); for (uint_fast8_t i = 0; i < aBytesPerLine; i++) { - unsigned char tCharacterToPrint = aMemory[tIndex + i]; + uint8_t tCharacterToPrint = aMemory[tIndex + i]; // if(isalnum(tIndex+i)){ // requires 40 bytes more program space if (' ' <= tCharacterToPrint && tCharacterToPrint <= '~') { - Serial.print(tCharacterToPrint); + Serial.print((char)tCharacterToPrint); } else if (tCharacterToPrint != 0x00 && tCharacterToPrint != 0xFF) { // for non printable characters except 0 and FF Serial.print('.'); diff --git a/JK-BMSToPylontechCAN/JK-BMS.h b/JK-BMSToPylontechCAN/JK-BMS.h index 8454f7a..3f89e33 100644 --- a/JK-BMSToPylontechCAN/JK-BMS.h +++ b/JK-BMSToPylontechCAN/JK-BMS.h @@ -1,7 +1,7 @@ /* * JK-BMS.h * - * Definitions of the data structures used by JK-BMS and the converter + * Definitions of the JK-BMS class and the data structures used by JK-BMS and the converter * * We use 6 Structures: * 1. JKReplyStruct - the main reply structure, containing raw BMS reply data Big Endian, which must be swapped. @@ -50,31 +50,11 @@ #define JK_FRAME_START_BYTE_1 0x57 #define JK_FRAME_END_BYTE 0x68 -void requestJK_BMSStatusFrame(SoftwareSerialTX *aSerial, bool aDebugModeActive = false); - -void initJKReplyFrameBuffer(); -void printJKReplyFrameBuffer(); - -#define JK_BMS_RECEIVE_OK 0 -#define JK_BMS_RECEIVE_FINISHED 1 -#define JK_BMS_RECEIVE_ERROR 2 -uint8_t readJK_BMSStatusFrameByte(); -void fillJKComputedData(); -void initializeComputedData(); - -extern uint16_t sReplyFrameBufferIndex; // Index of next byte to write to array, thus starting with 0. extern uint8_t JKReplyFrameBuffer[350]; // The raw big endian data as received from JK BMS -extern struct JKReplyStruct *sJKFAllReplyPointer; -extern bool sJKBMSFrameHasTimeout; // For sending CAN data -extern bool sPrintAlarmInfoOnlyOnce; -extern bool sAlarmJustGetsActive; extern char sUpTimeString[12]; // "1000D23H12M" is 11 bytes long extern bool sUpTimeStringMinuteHasChanged; -int16_t getJKTemperature(uint16_t aJKRAWTemperature); -int16_t getCurrent(uint16_t aJKRAWCurrent); - uint8_t swap(uint8_t aByte); uint16_t swap(uint16_t aWordToSwapBytes); uint32_t swap(uint32_t aLongToSwapBytes); @@ -91,17 +71,9 @@ void myPrintSwap(const __FlashStringHelper *aPGMString, int16_t a16BitValue); void myPrintIntAsFloatSwap(const __FlashStringHelper *aPGMString, int16_t a16BitValue); void myPrintlnSwap(const __FlashStringHelper *aPGMString, uint32_t a32BitValue); -void computeUpTimeString(); -void printJKStaticInfo(); -void printJKDynamicInfo(); -void detectAndPrintAlarmInfo(); -#if defined(ENABLE_MONITORING) -void printCSVLine(char aLeadingChar = '\0'); -#endif - struct JKCellInfoStruct { uint16_t CellMillivolt; -#if !defined(USE_NO_LCD) +#if defined(USE_SERIAL_2004_LCD) uint8_t VoltageIsMinMaxOrBetween; // One of VOLTAGE_IS_MINIMUM, VOLTAGE_IS_MAXIMUM or VOLTAGE_IS_BETWEEN_MINIMUM_AND_MAXIMUM #endif }; @@ -116,8 +88,6 @@ struct JKConvertedCellInfoStruct { uint16_t DeltaCellMillivolt; // Difference between MinimumVoltagCell and MaximumVoltagCell uint16_t RoundedAverageCellMillivolt; }; -extern struct JKConvertedCellInfoStruct JKConvertedCellInfo; // The converted little endian cell voltage data -void fillJKConvertedCellInfo(); #define VOLTAGE_IS_BETWEEN_MINIMUM_AND_MAXIMUM 0 #define VOLTAGE_IS_MINIMUM 1 @@ -129,12 +99,12 @@ void fillJKConvertedCellInfo(); * To identify runaway cells */ struct CellStatisticsStruct { -uint16_t CellMinimumArray[MAXIMUM_NUMBER_OF_CELLS]; // Count of cell minimums -uint16_t CellMaximumArray[MAXIMUM_NUMBER_OF_CELLS]; -uint8_t CellMinimumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; // Percentage of cell minimums -uint8_t CellMaximumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; -uint32_t BalancingCount; // Count of active balancing in SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS (2 seconds) units -uint32_t LastPrintedBalancingCount; // For printing with printJKDynamicInfo() + uint16_t CellMinimumArray[MAXIMUM_NUMBER_OF_CELLS]; // Count of cell minimums + uint16_t CellMaximumArray[MAXIMUM_NUMBER_OF_CELLS]; + uint8_t CellMinimumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; // Percentage of cell minimums + uint8_t CellMaximumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; + uint32_t BalancingCount; // Count of active balancing in SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS (2 seconds) units + uint32_t LastPrintedBalancingCount; // For printing with printJKDynamicInfo() }; #define MINIMUM_BALANCING_COUNT_FOR_DISPLAY 60 // 120 seconds / 2 minutes of balancing @@ -146,6 +116,11 @@ uint32_t LastPrintedBalancingCount; // For printing with printJKDynamicInfo() #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 +#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 + /* * All 16 and 32 bit values are stored byte swapped, i.e. MSB is stored in lower address. * Must be read with swap() @@ -189,10 +164,8 @@ struct JKComputedDataStruct { 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. }; -extern struct JKComputedDataStruct JKComputedData; // All derived converted and computed data useful for display #define AMPERE_HOUR_AS_ACCUMULATOR_10_MILLIAMPERE (3600L * 100 * MILLIS_IN_ONE_SECOND / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) // 180000 -int32_t getOnePercentCapacityAsAccumulator10Milliampere(); struct JKLastPrintedDataStruct { int16_t TemperaturePowerMosFet; // Degree Celsius @@ -202,7 +175,6 @@ struct JKLastPrintedDataStruct { int16_t BatteryLoadPower; // Watt Computed value, Charging is positive discharging is negative int32_t BatteryCapacityAccumulator10MilliAmpere; // For CSV line to print every 1% }; -extern struct JKLastPrintedDataStruct JKLastPrintedData; /* * Only for documentation @@ -247,6 +219,13 @@ union BMSStatusUnion { #define NUMBER_OF_DEFINED_ALARM_BITS 14 #define NO_ALARM_WORD_CONTENT 0x00 +#define MASK_OF_CHARGING_AND_DISCHARGING_OVERVOLTAGE_ALARM_UNSWAPPED 0x0C00 +// Required for displaying specific info for this alarms +#define INDEX_OF_CHARGING_OVERVOLTAGE_ALARM 2 +#define MASK_OF_CHARGING_OVERVOLTAGE_ALARM_UNSWAPPED 0x0800 +#define INDEX_OF_DISCHARGING_UNDERVOLTAGE_ALARM 3 +#define MASK_OF_DISCHARGING_UNDERVOLTAGE_ALARM_UNSWAPPED 0x0400 +#define INDEX_NO_ALARM 0xFF struct JKReplyStruct { uint8_t TokenTemperaturePowerMosFet; // 0x80 @@ -276,11 +255,11 @@ struct JKReplyStruct { union { uint16_t AlarmsAsWord; struct { - uint8_t AlarmsHighByte; - uint8_t AlarmsLowByte; + 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, but low byte of AlarmsAsWord + // 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 @@ -290,7 +269,7 @@ struct JKReplyStruct { bool Reserved1Alarm :1; // Two highest bits are reserved bool Reserved2Alarm :1; // 0x8000 - // Low byte of alarms, but high byte of AlarmsAsWord + // 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 % @@ -460,7 +439,7 @@ struct JKLastReplyStruct { union { // 0x8B uint16_t AlarmsAsWord; struct { - // High byte of alarms + // 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 @@ -470,7 +449,7 @@ struct JKLastReplyStruct { bool Reserved1Alarm :1; // Two highest bits are reserved bool Reserved2Alarm :1; - // Low byte of alarms + // 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 % @@ -501,4 +480,114 @@ struct JKLastReplyStruct { 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_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 RequestStatusFrame(bool aDebugModeActive); + uint8_t readJK_BMSStatusFrameByte(); + uint8_t checkForReplyFromBMSOrTimeout(); + + void printJKReplyFrameBuffer(); + + /* + * Processing functions + */ + void fillJKComputedData(); + void fillJKConvertedCellInfo(); + void computeUpTimeString(); + void processReceivedData(); + + /* + * Print functions + */ + void printEnabledState(bool aIsEnabled); + void printActiveState(bool aIsActive); + +#if defined(ENABLE_MONITORING) + void setCSVString(); + void printCSVLine(char aLeadingChar = '\0'); +#endif +#if !defined(NO_CELL_STATISTICS) + void printJKCellStatisticsInfo(); +#endif + void printJKCellInfoOverview(); + void printJKCellInfo(); + void printVoltageProtectionInfo(); + void printTemperatureProtectionInfo(); + void printBatteryInfo(); + void printBMSInfo(); + void printMiscellaneousInfo(); + void detectAndPrintAlarmInfo(); + + void printJKStaticInfo(); + void printJKDynamicInfo(); + + int32_t getOnePercentCapacityAsAccumulator10Milliampere(); + int16_t getJKTemperature(uint16_t aJKRAWTemperature); + int16_t getCurrent(uint16_t aJKRAWCurrent); + + /* + * 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. + /* + * 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. +#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 + + /* + * 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. + + /* + * BMS communication timeout + */ + uint32_t MillisOfLastReceivedByte = 0; // For timeout detection + + /* + * Use a 115200 baud software serial for the short request frame. + * If available, we also can use a second hardware serial here :-). + */ + SoftwareSerialTX TxToJKBMS; + +#if defined(HANDLE_MULTIPLE_BMS) + uint8_t NumberOfBMS; // The number of the BMS this class is used for. Starting with 1. +#endif + + /* + * The JKFrameAllDataStruct starts behind the header + cell data header 0x79 + CellInfoSize + * + the variable length cell data 3 bytes per cell, (CellInfoSize is contained in JKReplyFrameBuffer[12]) + */ + JKReplyStruct *JKAllReplyPointer = reinterpret_cast(&JKReplyFrameBuffer[JK_BMS_FRAME_HEADER_LENGTH + 2 + 48]); // assume 16 cells + JKLastReplyStruct lastJKReply; + JKComputedDataStruct JKComputedData; // All derived converted and computed data useful for display + JKLastPrintedDataStruct JKLastPrintedData; // For detecting changes for printing + JKConvertedCellInfoStruct JKConvertedCellInfo; // The converted little endian cell voltage data +#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 eef5e96..61a179f 100644 --- a/JK-BMSToPylontechCAN/JK-BMS.hpp +++ b/JK-BMSToPylontechCAN/JK-BMS.hpp @@ -37,12 +37,22 @@ #include #include "JK-BMS.h" -#if !defined(USE_NO_LCD) -#include "JK-BMS_LCD.h" // for sLCDDisplayPageNumber and JK_BMS_PAGE_OVERVIEW +#include "HexDump.hpp" // include sources for printBufferHex() + +#define USE_SOFTWARE_SERIAL +#if defined(USE_SOFTWARE_SERIAL) +/* + * Software serial for JK-BMS request frame sending + */ +#include "SoftwareSerialTX.h" +#else #endif -JKLastReplyStruct lastJKReply; +#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" @@ -58,13 +68,8 @@ uint8_t JKRequestStatusFrame[21] = { 0x4E, 0x57 /*4E 57 = StartOfFrame*/, 0x00, /* * Size of reply is 291 bytes for 16 cells. sizeof(JKReplyStruct) is 221. */ -uint16_t sReplyFrameBufferIndex = 0; // Index of next byte to write to array, except for last byte received. Starting with 0. -uint16_t sReplyFrameLength; // Received length of frame uint8_t JKReplyFrameBuffer[350]; // The raw big endian data as received from JK BMS. -JKComputedDataStruct JKComputedData; // All derived converted and computed data useful for display -JKLastPrintedDataStruct JKLastPrintedData; // For detecting changes for printing - #define LENGTH_OF_UPTIME_STRING 11 char sUpTimeString[LENGTH_OF_UPTIME_STRING + 1]; // "9999D23H12M" is 11 bytes long char sBalancingTimeString[11] = { ' ', ' ', '0', 'D', '0', '0', 'H', '0', '0', 'M', '\0' }; // "999D23H12M" is 10 bytes long @@ -74,17 +79,6 @@ char sLastUpTimeTenthOfMinuteCharacter; // For detecting changes in string a bool sUpTimeStringHourHasChanged; char sLastUpTimeHourCharacter; // For setting sUpTimeStringHourHasChanged -JKConvertedCellInfoStruct JKConvertedCellInfo; // The converted little endian cell voltage data -#if !defined(NO_CELL_STATISTICS) -struct CellStatisticsStruct CellStatistics; -#endif //NO_CELL_STATISTICS - -/* - * The JKFrameAllDataStruct starts behind the header + cell data header 0x79 + CellInfoSize - * + the variable length cell data 3 bytes per cell, (CellInfoSize is contained in JKReplyFrameBuffer[12]) - */ -JKReplyStruct *sJKFAllReplyPointer = reinterpret_cast(&JKReplyFrameBuffer[JK_BMS_FRAME_HEADER_LENGTH + 2 + 48]); // assume 16 cells - /* * ALARM stuff */ @@ -103,181 +97,282 @@ const char CellUndervoltage[] PROGMEM = "Cell undervoltage"; // B const char _309AProtection[] PROGMEM = "309_A protection"; // Byte 1.4, const char _309BProtection[] PROGMEM = "309_B protection"; // Byte 1.5, -/* - * Since over and undervoltage is indicated by O or U in state info, it is not necessary to switch to Overview page - */ -//#define ENABLE_OVER_AND_UNDER_VOLTAGE_WARNING_ON_LCD // Enables switching to Overview page and showing over- and undervoltage data. -#define MASK_OF_CHARGING_AND_DISCHARGING_OVERVOLTAGE_ALARM_UNSWAPPED 0x0C00 -// Required for displaying specific info for this alarms -#define INDEX_OF_CHARGING_OVERVOLTAGE_ALARM 2 -#define MASK_OF_CHARGING_OVERVOLTAGE_ALARM_UNSWAPPED 0x0800 -#define INDEX_OF_DISCHARGING_UNDERVOLTAGE_ALARM 3 -#define MASK_OF_DISCHARGING_UNDERVOLTAGE_ALARM_UNSWAPPED 0x0400 -#define INDEX_NO_ALARM 0xFF - const char *const JK_BMSAlarmStringsArray[NUMBER_OF_DEFINED_ALARM_BITS] PROGMEM = { lowCapacity, MosFetOvertemperature, chargingOvervoltage, dischargingUndervoltage, Sensor2Overtemperature, chargingOvercurrent, dischargingOvercurrent, CellVoltageDifference, Sensor1Overtemperature, Sensor2LowLemperature, CellOvervoltage, CellUndervoltage, _309AProtection, _309BProtection }; /* - * Flags for alarm handling - */ -/* - * 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. + * Helper macro for getting a macro definition as string */ -bool sAlarmJustGetsActive = false; // True if alarm bits changed and any alarm is still active. False if alarm bits changed and no alarm is active. -#if defined(USE_SERIAL_2004_LCD) -uint8_t sAlarmIndexToShowOnLCD = INDEX_NO_ALARM; // Index of current alarm to show with Alarm / Overview page. Set by detectAndPrintAlarmInfo() and reset on page switch. -bool sPrintAlarmInfoOnlyOnce = false; // True -> fill main part of LCD alarm page only once. Is set by detectAndPrintAlarmInfo(). -bool sShowAlarmInsteadOfOverview = false; // is reset on button press -#endif -#if defined(SUPPRESS_CONSECUTIVE_SAME_ALARMS) -uint16_t sLastActiveAlarmsAsWord; // Used to disable new alarm beep on recurring of same alarm -#define SUPPRESS_CONSECUTIVE_SAME_ALARMS_TIMEOUT_SECONDS 3600 // Allow consecutive same alarms after 1 hour of no alarm -uint16_t sNoAlarmCounter; // Counts no alarms in order to reset suppression of consecutive alarms. +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) + +JK_BMS::JK_BMS() { +#if defined(HANDLE_MULTIPLE_BMS) + NumbersOfBMSInstances++; + NumberOfBMS = NumbersOfBMSInstances; #endif +} /* - * Helper macro for getting a macro definition as string + * 115200 baud soft serial to JK-BMS. For serial from BMS we use the hardware Serial RX. */ -#define STR_HELPER(x) #x -#define STR(x) STR_HELPER(x) +void JK_BMS::init(uint8_t aTxPinNumber) { +#if defined(USE_SOFTWARE_SERIAL) + TxToJKBMS.setTX(aTxPinNumber); + TxToJKBMS.begin(115200); +#else +#endif +} /* * 1.85 ms */ -void requestJK_BMSStatusFrame(SoftwareSerialTX *aSerial, bool aDebugModeActive) { +void JK_BMS::requestJK_BMSStatusFrame(bool aDebugModeActive) { if (aDebugModeActive) { Serial.println(); - Serial.println(F("Send requestFrame with TxToJKBMS")); +#if defined(HANDLE_MULTIPLE_BMS) + Serial.print(F("Send requestFrame to BMS ")); + Serial.println(NumberOfBMS); +#else + Serial.println(F("Send requestFrame to BMS")); +#endif printBufferHex(JKRequestStatusFrame, sizeof(JKRequestStatusFrame)); } Serial.flush(); for (uint8_t i = 0; i < sizeof(JKRequestStatusFrame); ++i) { - aSerial->write(JKRequestStatusFrame[i]); +#if defined(USE_SOFTWARE_SERIAL) + TxToJKBMS.write(JKRequestStatusFrame[i]); +#else +#endif } } -void initJKReplyFrameBuffer() { - sReplyFrameBufferIndex = 0; -} - /* - * Prints formatted reply buffer raw content + * Request status frame */ -void printJKReplyFrameBuffer() { - uint8_t *tBufferAddress = JKReplyFrameBuffer; - printBufferHex(tBufferAddress, JK_BMS_FRAME_HEADER_LENGTH); - - tBufferAddress += JK_BMS_FRAME_HEADER_LENGTH; - printBufferHex(tBufferAddress, JK_BMS_FRAME_CELL_INFO_LENGTH); - - tBufferAddress += JK_BMS_FRAME_CELL_INFO_LENGTH; - uint8_t tCellInfoLength = JKReplyFrameBuffer[JK_BMS_FRAME_INDEX_OF_CELL_INFO_LENGTH]; - printBufferHex(tBufferAddress, tCellInfoLength); // Cell info - Serial.println(); - - tBufferAddress += tCellInfoLength; - int16_t tRemainingDataLength = ((int16_t) sReplyFrameBufferIndex + 1) - - (JK_BMS_FRAME_HEADER_LENGTH + JK_BMS_FRAME_CELL_INFO_LENGTH + JK_BMS_FRAME_TRAILER_LENGTH + tCellInfoLength); - if (tRemainingDataLength <= 0) { - return; +void JK_BMS::RequestStatusFrame(bool aDebugModeActive) { + /* + * Flush input buffer (e.g. after JK_BMS_RECEIVE_ERROR) and send request to JK-BMS + */ + while (Serial.available()) { + Serial.read(); } - printBufferHex(tBufferAddress, tRemainingDataLength); - - tBufferAddress += tRemainingDataLength; - printBufferHex(tBufferAddress, JK_BMS_FRAME_TRAILER_LENGTH); // Trailer +#if defined(TIMING_TEST) + digitalWriteFast(TIMING_TEST_PIN, HIGH); +#endif + /* + * Copy last complete reply and computed values for change determination + */ + lastJKReply.SOCPercent = JKAllReplyPointer->SOCPercent; + lastJKReply.AlarmUnion.AlarmsAsWord = JKAllReplyPointer->AlarmUnion.AlarmsAsWord; + lastJKReply.BMSStatus.StatusAsWord = JKAllReplyPointer->BMSStatus.StatusAsWord; + lastJKReply.SystemWorkingMinutes = JKAllReplyPointer->SystemWorkingMinutes; + + requestJK_BMSStatusFrame(aDebugModeActive); // 1.85 ms +#if defined(TIMING_TEST) + digitalWriteFast(TIMING_TEST_PIN, LOW); +#endif + ReplyFrameBufferIndex = 0; // init reply frame buffer + MillisOfLastReceivedByte = millis(); // initialize reply timeout } -#define JK_BMS_RECEIVE_OK 0 -#define JK_BMS_RECEIVE_FINISHED 1 -#define JK_BMS_RECEIVE_ 2 /* * Is assumed to be called if Serial.available() is true - * @return JK_BMS_RECEIVE_OK, if still receiving; JK_BMS_RECEIVE_FINISHED, if complete frame was successfully read - * JK_BMS_RECEIVE_, if frame has s. + * @return JK_BMS_RECEIVE_ONGOING, if still receiving; JK_BMS_RECEIVE_FINISHED, if complete frame was successfully read + * JK_BMS_RECEIVE_ERROR, if frame plausibility does not hold. * Reply starts 0.18 ms to 0.45 ms after request was received */ -uint8_t readJK_BMSStatusFrameByte() { +uint8_t JK_BMS::readJK_BMSStatusFrameByte() { uint8_t tReceivedByte = Serial.read(); - JKReplyFrameBuffer[sReplyFrameBufferIndex] = tReceivedByte; + JKReplyFrameBuffer[ReplyFrameBufferIndex] = tReceivedByte; /* * Plausi check and get length of frame */ - if (sReplyFrameBufferIndex == 0) { + if (ReplyFrameBufferIndex == 0) { // start byte 1 if (tReceivedByte != JK_FRAME_START_BYTE_0) { Serial.println(F(" start frame token != 0x4E")); - return JK_BMS_RECEIVE_; + return JK_BMS_RECEIVE_ERROR; } - } else if (sReplyFrameBufferIndex == 1) { + } else if (ReplyFrameBufferIndex == 1) { if (tReceivedByte != JK_FRAME_START_BYTE_1) { // - return JK_BMS_RECEIVE_; + return JK_BMS_RECEIVE_ERROR; } - } else if (sReplyFrameBufferIndex == 3) { + } else if (ReplyFrameBufferIndex == 3) { // length of frame - sReplyFrameLength = (JKReplyFrameBuffer[2] << 8) + tReceivedByte; + ReplyFrameLength = (JKReplyFrameBuffer[2] << 8) + tReceivedByte; - } else if (sReplyFrameLength > MINIMAL_JK_BMS_FRAME_LENGTH && sReplyFrameBufferIndex == sReplyFrameLength - 3) { + } else if (ReplyFrameLength > MINIMAL_JK_BMS_FRAME_LENGTH && ReplyFrameBufferIndex == ReplyFrameLength - 3) { // Check end token 0x68 if (tReceivedByte != JK_FRAME_END_BYTE) { Serial.print(F(" end frame token 0x")); Serial.print(tReceivedByte, HEX); Serial.print(F(" at index")); - Serial.print(sReplyFrameBufferIndex); - Serial.print(F(" is != 0x68. sReplyFrameLength= ")); - Serial.print(sReplyFrameLength); + Serial.print(ReplyFrameBufferIndex); + Serial.print(F(" is != 0x68. ReplyFrameLength= ")); + Serial.print(ReplyFrameLength); Serial.print(F(" | 0x")); - Serial.println(sReplyFrameLength, HEX); - return JK_BMS_RECEIVE_; + Serial.println(ReplyFrameLength, HEX); + return JK_BMS_RECEIVE_ERROR; } - } else if (sReplyFrameLength > MINIMAL_JK_BMS_FRAME_LENGTH && sReplyFrameBufferIndex == sReplyFrameLength + 1) { + } else if (ReplyFrameLength > MINIMAL_JK_BMS_FRAME_LENGTH && ReplyFrameBufferIndex == ReplyFrameLength + 1) { /* * Frame received completely, perform checksum check */ uint16_t tComputedChecksum = 0; - for (uint16_t i = 0; i < sReplyFrameLength - 2; i++) { + for (uint16_t i = 0; i < ReplyFrameLength - 2; i++) { tComputedChecksum = tComputedChecksum + JKReplyFrameBuffer[i]; } - uint16_t tReceivedChecksum = (JKReplyFrameBuffer[sReplyFrameLength] << 8) + tReceivedByte; + uint16_t tReceivedChecksum = (JKReplyFrameBuffer[ReplyFrameLength] << 8) + tReceivedByte; if (tComputedChecksum != tReceivedChecksum) { Serial.print(F("Checksum , computed checksum=0x")); Serial.print(tComputedChecksum, HEX); Serial.print(F(", received checksum=0x")); Serial.println(tReceivedChecksum, HEX); - return JK_BMS_RECEIVE_; + return JK_BMS_RECEIVE_ERROR; } else { + /* + * Checksum OK, transfer finished :-) + */ return JK_BMS_RECEIVE_FINISHED; } } - sReplyFrameBufferIndex++; - return JK_BMS_RECEIVE_OK; + ReplyFrameBufferIndex++; + return JK_BMS_RECEIVE_ONGOING; +} + +/* + * Check for byte at serial input if response is expected. + * @return JK_BMS_RECEIVE_ONGOING, if still receiving; JK_BMS_RECEIVE_FINISHED, if complete frame was successfully read and processed + * JK_BMS_RECEIVE_TIMEOUT if timeout happened; JK_BMS_RECEIVE_ERROR, if frame plausibility does not hold. + * If first timeout, set TimeoutJustDetected + */ +uint8_t JK_BMS::checkForReplyFromBMSOrTimeout() { + if (Serial.available()) { + MillisOfLastReceivedByte = millis(); + uint8_t tReceiveResultCode = readJK_BMSStatusFrameByte(); + if (tReceiveResultCode == JK_BMS_RECEIVE_FINISHED) { + /* + * All JK-BMS status frame data received + */ + if (JKBMSFrameHasTimeout) { + JK_INFO_PRINTLN(F("Successfully receiving first BMS frame after communication timeout")); + // First successful response frame after timeout :-) + JKBMSFrameHasTimeout = false; + TimeoutJustDetected = false; + } + + } else if (tReceiveResultCode != JK_BMS_RECEIVE_ONGOING) { + /* + * Error here + */ + Serial.print(F("Receive error=")); + Serial.print(tReceiveResultCode); + Serial.print(F(" at index")); + Serial.println(ReplyFrameBufferIndex); + + printJKReplyFrameBuffer(); + } + return tReceiveResultCode; + } else if (millis() - MillisOfLastReceivedByte >= TIMEOUT_MILLIS_FOR_FRAME_REPLY) { + /* + * Here we have timeout at receiving. We requested response frame, but serial was not available fore a longer time. + */ + if (!JKBMSFrameHasTimeout) { + //Do this only once per timeout + JKBMSFrameHasTimeout = true; + TimeoutJustDetected = true; // This forces the beep + } + return JK_BMS_RECEIVE_TIMEOUT; + } + return JK_BMS_RECEIVE_ONGOING; // No requested or ongoing receive +} + +void JK_BMS::processReceivedData() { + /* + * Set the static pointer to the start of the reply data which depends on the number of cell voltage entries + * The JKFrameAllDataStruct starts behind the header + cell data header 0x79 + CellInfoSize + the variable length cell data 3 bytes per cell, (CellInfoSize is contained in JKReplyFrameBuffer[12]) + */ + JKAllReplyPointer = reinterpret_cast(&JKReplyFrameBuffer[JK_BMS_FRAME_HEADER_LENGTH + 2 + + JKReplyFrameBuffer[JK_BMS_FRAME_INDEX_OF_CELL_INFO_LENGTH]]); + + fillJKConvertedCellInfo(); + /* + * Print newline, if SOC changed + */ + if (JKAllReplyPointer->SOCPercent != lastJKReply.SOCPercent) { + Serial.println(); + } + fillJKComputedData(); + + computeUpTimeString(); + detectAndPrintAlarmInfo(); // UpTimeString is used here +} + +/* + * Prints formatted reply buffer raw content + * 11 bytes header + * 2 bytes cell info length + * Cell info + * Blank line + * Other content + * 9 bytes trailer + */ +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); +#else + Serial.println(F(" bytes received")); +#endif + printBufferHex(tBufferAddress, JK_BMS_FRAME_HEADER_LENGTH); + + tBufferAddress += JK_BMS_FRAME_HEADER_LENGTH; + printBufferHex(tBufferAddress, JK_BMS_FRAME_CELL_INFO_LENGTH); + + tBufferAddress += JK_BMS_FRAME_CELL_INFO_LENGTH; + uint8_t tCellInfoLength = JKReplyFrameBuffer[JK_BMS_FRAME_INDEX_OF_CELL_INFO_LENGTH]; + printBufferHex(tBufferAddress, tCellInfoLength); // Cell info + Serial.println(); + + tBufferAddress += tCellInfoLength; + int16_t tRemainingDataLength = ((int16_t) ReplyFrameBufferIndex + 1) + - (JK_BMS_FRAME_HEADER_LENGTH + JK_BMS_FRAME_CELL_INFO_LENGTH + JK_BMS_FRAME_TRAILER_LENGTH + tCellInfoLength); + if (tRemainingDataLength <= 0) { + return; + } + printBufferHex(tBufferAddress, tRemainingDataLength); + + tBufferAddress += tRemainingDataLength; + printBufferHex(tBufferAddress, JK_BMS_FRAME_TRAILER_LENGTH); // Trailer } /* * Highest bit is set means charge * @return Charge is positive, discharge is negative */ -int16_t getCurrent(uint16_t aJKRAWCurrent) { +int16_t JK_BMS::getCurrent(uint16_t aJKRAWCurrent) { uint16_t tCurrent = swap(aJKRAWCurrent); if (tCurrent == 0 || (tCurrent & 0x8000) == 0x8000) { // Charge - NO two's complement! return (tCurrent & 0x7FFF); } - // discharge +// discharge return tCurrent * -1; } -int16_t getJKTemperature(uint16_t aJKRAWTemperature) { +int16_t JK_BMS::getJKTemperature(uint16_t aJKRAWTemperature) { uint16_t tTemperature = swap(aJKRAWTemperature); if (tTemperature <= 100) { return tTemperature; @@ -285,7 +380,7 @@ int16_t getJKTemperature(uint16_t aJKRAWTemperature) { return 100 - tTemperature; } -int32_t getOnePercentCapacityAsAccumulator10Milliampere() { +int32_t JK_BMS::getOnePercentCapacityAsAccumulator10Milliampere() { return (AMPERE_HOUR_AS_ACCUMULATOR_10_MILLIAMPERE / 100) * JKComputedData.TotalCapacityAmpereHour; } @@ -312,6 +407,7 @@ void myPrintln(const __FlashStringHelper *aPGMString, uint8_t a8BitValue) { Serial.println(a8BitValue); } +// Not used yet, since we print uint8_t + 1 which is int16_t void myPrint(const __FlashStringHelper *aPGMString, uint8_t a8BitValue) { Serial.print(aPGMString); Serial.print(a8BitValue); @@ -371,7 +467,7 @@ void myPrintlnSwap(const __FlashStringHelper *aPGMString, uint32_t a32BitValue) * Convert the big endian cell voltage data from JKReplyFrameBuffer to little endian data in JKConvertedCellInfo * and compute minimum, maximum, delta, and average */ -void fillJKConvertedCellInfo() { +void JK_BMS::fillJKConvertedCellInfo() { // uint8_t *tJKCellInfoReplyPointer = &TestJKReplyStatusFrame[11]; uint8_t *tJKCellInfoReplyPointer = &JKReplyFrameBuffer[JK_BMS_FRAME_INDEX_OF_CELL_INFO_LENGTH]; @@ -413,19 +509,19 @@ void fillJKConvertedCellInfo() { JKConvertedCellInfo.DeltaCellMillivolt = tMaximumMillivolt - tMinimumMillivolt; JKConvertedCellInfo.RoundedAverageCellMillivolt = (tMillivoltSum + (tNumberOfNonNullCellInfo / 2)) / tNumberOfNonNullCellInfo; -#if !defined(NO_CELL_STATISTICS) && !defined(USE_NO_LCD) +#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 (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { + 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 (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { + if (JKAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { CellStatistics.CellMaximumArray[i]++; } } else { @@ -527,7 +623,7 @@ void fillJKConvertedCellInfo() { /* * Print formatted cell info on Serial */ -void printJKCellStatisticsInfo() { +void JK_BMS::printJKCellStatisticsInfo() { uint8_t tNumberOfCellInfo = JKConvertedCellInfo.ActualNumberOfCellInfoEntries; /* @@ -562,50 +658,48 @@ void printJKCellStatisticsInfo() { #endif // NO_CELL_STATISTICS -void initializeComputedData() { - // Initialize capacity accumulator with sensible value - JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere = (AMPERE_HOUR_AS_ACCUMULATOR_10_MILLIAMPERE / 100) - * sJKFAllReplyPointer->SOCPercent * JKComputedData.TotalCapacityAmpereHour; -} - -void fillJKComputedData() { - JKComputedData.TemperaturePowerMosFet = getJKTemperature(sJKFAllReplyPointer->TemperaturePowerMosFet); +void JK_BMS::fillJKComputedData() { + JKComputedData.TemperaturePowerMosFet = getJKTemperature(JKAllReplyPointer->TemperaturePowerMosFet); int16_t tMaxTemperature = JKComputedData.TemperaturePowerMosFet; - JKComputedData.TemperatureSensor1 = getJKTemperature(sJKFAllReplyPointer->TemperatureSensor1); + JKComputedData.TemperatureSensor1 = getJKTemperature(JKAllReplyPointer->TemperatureSensor1); if (tMaxTemperature < JKComputedData.TemperatureSensor1) { tMaxTemperature = JKComputedData.TemperatureSensor1; } - JKComputedData.TemperatureSensor2 = getJKTemperature(sJKFAllReplyPointer->TemperatureSensor2); + JKComputedData.TemperatureSensor2 = getJKTemperature(JKAllReplyPointer->TemperatureSensor2); if (tMaxTemperature < JKComputedData.TemperatureSensor2) { tMaxTemperature = JKComputedData.TemperatureSensor2; } JKComputedData.TemperatureMaximum = tMaxTemperature; - JKComputedData.TotalCapacityAmpereHour = swap(sJKFAllReplyPointer->TotalCapacityAmpereHour); + JKComputedData.TotalCapacityAmpereHour = swap(JKAllReplyPointer->TotalCapacityAmpereHour); // 16 bit multiplication gives overflow at 640 Ah - JKComputedData.RemainingCapacityAmpereHour = ((uint32_t) JKComputedData.TotalCapacityAmpereHour - * sJKFAllReplyPointer->SOCPercent) / 100; + JKComputedData.RemainingCapacityAmpereHour = ((uint32_t) JKComputedData.TotalCapacityAmpereHour * JKAllReplyPointer->SOCPercent) + / 100; // Two values which are zero during JK-BMS startup for around 16 seconds - JKComputedData.BMSIsStarting = (sJKFAllReplyPointer->SOCPercent == 0 && sJKFAllReplyPointer->Cycles == 0); + JKComputedData.BMSIsStarting = (JKAllReplyPointer->SOCPercent == 0 && JKAllReplyPointer->Cycles == 0); + +// Initialize capacity accumulator with sensible value + JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere = (AMPERE_HOUR_AS_ACCUMULATOR_10_MILLIAMPERE / 100) + * JKAllReplyPointer->SOCPercent * JKComputedData.TotalCapacityAmpereHour; - JKComputedData.BatteryFullVoltage10Millivolt = swap(sJKFAllReplyPointer->BatteryOvervoltageProtection10Millivolt); - JKComputedData.BatteryVoltage10Millivolt = swap(sJKFAllReplyPointer->Battery10Millivolt); + JKComputedData.BatteryFullVoltage10Millivolt = swap(JKAllReplyPointer->BatteryOvervoltageProtection10Millivolt); + JKComputedData.BatteryVoltage10Millivolt = swap(JKAllReplyPointer->Battery10Millivolt); // JKComputedData.BatteryVoltageDifferenceToFull10Millivolt = JKComputedData.BatteryFullVoltage10Millivolt // - JKComputedData.BatteryVoltage10Millivolt; - JKComputedData.BatteryEmptyVoltage10Millivolt = swap(sJKFAllReplyPointer->BatteryUndervoltageProtection10Millivolt); + JKComputedData.BatteryEmptyVoltage10Millivolt = swap(JKAllReplyPointer->BatteryUndervoltageProtection10Millivolt); JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt = JKComputedData.BatteryVoltage10Millivolt - JKComputedData.BatteryEmptyVoltage10Millivolt; JKComputedData.BatteryVoltageFloat = JKComputedData.BatteryVoltage10Millivolt / 100.0; - JKComputedData.Battery10MilliAmpere = getCurrent(sJKFAllReplyPointer->Battery10MilliAmpere); + JKComputedData.Battery10MilliAmpere = getCurrent(JKAllReplyPointer->Battery10MilliAmpere); JKComputedData.BatteryLoadCurrentFloat = JKComputedData.Battery10MilliAmpere / 100.0; JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere += JKComputedData.Battery10MilliAmpere; - if (lastJKReply.SOCPercent == 0 && sJKFAllReplyPointer->SOCPercent == 1) { + if (lastJKReply.SOCPercent == 0 && JKAllReplyPointer->SOCPercent == 1) { JK_INFO_PRINTLN(F("Reset capacity to 1%")); // Reset capacity at transition from 0 to 1 JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere = getOnePercentCapacityAsAccumulator10Milliampere(); @@ -613,9 +707,9 @@ void fillJKComputedData() { } // Serial.print("Battery10MilliAmpere=0x"); -// Serial.print(sJKFAllReplyPointer->Battery10MilliAmpere, HEX); +// Serial.print(JKAllReplyPointer->Battery10MilliAmpere, HEX); // Serial.print(" Battery10MilliAmpere swapped=0x"); -// Serial.println(swap(sJKFAllReplyPointer->Battery10MilliAmpere), HEX); +// Serial.println(swap(JKAllReplyPointer->Battery10MilliAmpere), HEX); // Serial.print(" Battery10MilliAmpere="); // Serial.print(JKComputedData.Battery10MilliAmpere); // Serial.print(" BatteryLoadCurrent="); @@ -627,7 +721,7 @@ void fillJKComputedData() { /* * Increment BalancingCount and fill sBalancingTimeString */ - if (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { + if (JKAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { CellStatistics.BalancingCount++; sprintf_P(sBalancingTimeString, PSTR("%3uD%02uH%02uM"), (uint16_t) (CellStatistics.BalancingCount / (60 * 24 * 30UL)), (uint16_t) ((CellStatistics.BalancingCount / (60 * 30)) % 24), @@ -636,7 +730,7 @@ void fillJKComputedData() { #endif // NO_CELL_STATISTICS } -void printJKCellInfoOverview() { +void JK_BMS::printJKCellInfoOverview() { myPrint(F(" Minimum at "), JKConvertedCellInfo.IndexOfMinimumCellMillivolt + 1); myPrint(F("="), JKConvertedCellInfo.MinimumCellMillivolt); myPrint(F(" mV, Maximum at "), JKConvertedCellInfo.IndexOfMaximumCellMillivolt + 1); @@ -648,12 +742,12 @@ void printJKCellInfoOverview() { /* * Print formatted cell info on Serial */ -void printJKCellInfo() { +void JK_BMS::printJKCellInfo() { uint8_t tNumberOfCellInfo = JKConvertedCellInfo.ActualNumberOfCellInfoEntries; Serial.print(JKConvertedCellInfo.ActualNumberOfCellInfoEntries); Serial.print(F(" Cells,")); - // Print summary +// Print summary printJKCellInfoOverview(); /* @@ -679,8 +773,8 @@ void printJKCellInfo() { Serial.println(); } -void printVoltageProtectionInfo() { - JKReplyStruct *tJKFAllReplyPointer = sJKFAllReplyPointer; +void JK_BMS::printVoltageProtectionInfo() { + JKReplyStruct *tJKFAllReplyPointer = JKAllReplyPointer; /* * Voltage protection */ @@ -707,8 +801,8 @@ void printVoltageProtectionInfo() { Serial.println(); } -void printTemperatureProtectionInfo() { - JKReplyStruct *tJKFAllReplyPointer = sJKFAllReplyPointer; +void JK_BMS::printTemperatureProtectionInfo() { + JKReplyStruct *tJKFAllReplyPointer = JKAllReplyPointer; /* * Temperature protection */ @@ -732,8 +826,8 @@ void printTemperatureProtectionInfo() { Serial.println(); } -void printBatteryInfo() { - JKReplyStruct *tJKFAllReplyPointer = sJKFAllReplyPointer; +void JK_BMS::printBatteryInfo() { + JKReplyStruct *tJKFAllReplyPointer = JKAllReplyPointer; Serial.print(F("Manufacturer Date=")); tJKFAllReplyPointer->TokenSystemWorkingMinutes = '\0'; // Set end of string token @@ -759,8 +853,8 @@ void printBatteryInfo() { Serial.println(); } -void printBMSInfo() { - JKReplyStruct *tJKFAllReplyPointer = sJKFAllReplyPointer; +void JK_BMS::printBMSInfo() { + JKReplyStruct *tJKFAllReplyPointer = JKAllReplyPointer; myPrintln(F("Protocol Version Number="), tJKFAllReplyPointer->ProtocolVersionNumber); @@ -777,8 +871,8 @@ void printBMSInfo() { Serial.println(); } -void printMiscellaneousInfo() { - JKReplyStruct *tJKFAllReplyPointer = sJKFAllReplyPointer; +void JK_BMS::printMiscellaneousInfo() { + JKReplyStruct *tJKFAllReplyPointer = JKAllReplyPointer; myPrintlnSwap(F("Balance Starting Cell Voltage[mV]="), tJKFAllReplyPointer->BalancingStartMillivolt); myPrintlnSwap(F("Balance Triggering Voltage Difference[mV]="), tJKFAllReplyPointer->BalancingStartDifferentialMillivolt); @@ -799,58 +893,23 @@ void printMiscellaneousInfo() { * ChargeOvervoltageAlarm is displayed as 'O' in printShortStateOnLCD() * ChargeUndervoltageAlarm is displayed as 'U' in printShortStateOnLCD() */ -void detectAndPrintAlarmInfo() { - JKReplyStruct *tJKFAllReplyPointer = sJKFAllReplyPointer; - -#if defined(SUPPRESS_CONSECUTIVE_SAME_ALARMS) - if (tJKFAllReplyPointer->AlarmUnion.AlarmsAsWord == NO_ALARM_WORD_CONTENT) { - sNoAlarmCounter++; // integer overflow does not really matter here - // sNoAlarmCounter == 1800 - Allow consecutive same alarms after 1 hour of no alarm - if (sNoAlarmCounter - == (SUPPRESS_CONSECUTIVE_SAME_ALARMS_TIMEOUT_SECONDS * MILLIS_IN_ONE_SECOND) / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) { - sLastActiveAlarmsAsWord = NO_ALARM_WORD_CONTENT; // Reset LastActiveAlarmsAsWord - } - } -#endif +void JK_BMS::detectAndPrintAlarmInfo() { + JKReplyStruct *tJKAllReplyPointer = JKAllReplyPointer; /* * Do it only once per change */ - if (tJKFAllReplyPointer->AlarmUnion.AlarmsAsWord != lastJKReply.AlarmUnion.AlarmsAsWord) { - if (tJKFAllReplyPointer->AlarmUnion.AlarmsAsWord == NO_ALARM_WORD_CONTENT) { + if (tJKAllReplyPointer->AlarmUnion.AlarmsAsWord != lastJKReply.AlarmUnion.AlarmsAsWord) { + if (tJKAllReplyPointer->AlarmUnion.AlarmsAsWord == NO_ALARM_WORD_CONTENT) { Serial.println(F("All alarms are cleared now")); - sAlarmJustGetsActive = false; + AlarmActive = false; } else { -#if defined(SUPPRESS_CONSECUTIVE_SAME_ALARMS) - sNoAlarmCounter = 0; // reset counter - // only start actions if new alarm is different - if(sLastActiveAlarmsAsWord == tJKFAllReplyPointer->AlarmUnion.AlarmsAsWord){ - sDoAlarmOrTimeoutBeep = true; // do only one beep for recurring alarms - } else { - /* - * At least one alarm is active and it is not the last alarm already processed - */ - sLastActiveAlarmsAsWord = tJKFAllReplyPointer->AlarmUnion.AlarmsAsWord; -#endif -#if defined(USE_SERIAL_2004_LCD) -# if defined(ENABLE_OVER_AND_UNDER_VOLTAGE_WARNING_ON_LCD) - sPrintAlarmInfoOnlyOnce = true; // This forces a switch to Alarm page -# else - if((tJKFAllReplyPointer->AlarmUnion.AlarmsAsWord & ~MASK_OF_CHARGING_AND_DISCHARGING_OVERVOLTAGE_ALARM_UNSWAPPED) - || sLCDDisplayPageNumber == JK_BMS_PAGE_OVERVIEW) { - // Other than over / undervoltage alarm bit is active - sPrintAlarmInfoOnlyOnce = true; // This forces display on Alarm page - } -# endif -#endif - sAlarmJustGetsActive = true; // This forces the beep -#if defined(SUPPRESS_CONSECUTIVE_SAME_ALARMS) - } -#endif + AlarmJustGetsActive = true; // This forces the beep, which is NOT suppressed for over and undervoltage + AlarmActive = true; /* * Print alarm info */ - uint16_t tAlarms = swap(tJKFAllReplyPointer->AlarmUnion.AlarmsAsWord); + uint16_t tAlarms = swap(tJKAllReplyPointer->AlarmUnion.AlarmsAsWord); Serial.println(F("*** ALARM FLAGS ***")); Serial.print(sUpTimeString); // print uptime to have a timestamp for the alarm Serial.print(F(": Alarm bits=0x")); @@ -866,7 +925,7 @@ void detectAndPrintAlarmInfo() { Serial.print(tAlarmMask, BIN); Serial.print(F(" -> ")); #if defined(USE_SERIAL_2004_LCD) - sAlarmIndexToShowOnLCD = i; + AlarmIndexToShowOnLCD = i; #endif Serial.println( reinterpret_cast((char*) (pgm_read_word(&JK_BMSAlarmStringsArray[i])))); @@ -880,7 +939,7 @@ void detectAndPrintAlarmInfo() { } } -void printEnabledState(bool aIsEnabled) { +void JK_BMS::printEnabledState(bool aIsEnabled) { if (aIsEnabled) { Serial.print(F(" enabled")); } else { @@ -888,7 +947,7 @@ void printEnabledState(bool aIsEnabled) { } } -void printActiveState(bool aIsActive) { +void JK_BMS::printActiveState(bool aIsActive) { if (!aIsActive) { Serial.print(F(" not")); } @@ -898,9 +957,15 @@ void printActiveState(bool aIsActive) { /* * Called exclusively once by processJK_BMSStatusFrame() */ -void printJKStaticInfo() { +void JK_BMS::printJKStaticInfo() { +#if defined(HANDLE_MULTIPLE_BMS) + Serial.print(F("*** BMS ")); + Serial.print(NumberOfBMS); + Serial.println(F(" INFO ***")); +#else Serial.println(F("*** BMS INFO ***")); +#endif printBMSInfo(); Serial.println(F("*** BATTERY INFO ***")); @@ -916,11 +981,11 @@ void printJKStaticInfo() { printMiscellaneousInfo(); } -void computeUpTimeString() { - if (sJKFAllReplyPointer->SystemWorkingMinutes != lastJKReply.SystemWorkingMinutes) { +void JK_BMS::computeUpTimeString() { + if (JKAllReplyPointer->SystemWorkingMinutes != lastJKReply.SystemWorkingMinutes) { sUpTimeStringMinuteHasChanged = true; - uint32_t tSystemWorkingMinutes = swap(sJKFAllReplyPointer->SystemWorkingMinutes); + uint32_t tSystemWorkingMinutes = swap(JKAllReplyPointer->SystemWorkingMinutes); // 1 kByte for sprintf creates string "1234D23H12M" sprintf_P(sUpTimeString, PSTR("%4uD%02uH%02uM"), (uint16_t) (tSystemWorkingMinutes / (60 * 24)), (uint16_t) ((tSystemWorkingMinutes / 60) % 24), (uint16_t) (tSystemWorkingMinutes % 60)); @@ -942,8 +1007,8 @@ extern const char sCSVCaption[] PROGMEM; * Use converted cell voltage info from JKConvertedCellInfo * All other data are used unconverted and are therefore printed by swap() functions. */ -void printJKDynamicInfo() { - JKReplyStruct *tJKFAllReplyPointer = sJKFAllReplyPointer; +void JK_BMS::printJKDynamicInfo() { + JKReplyStruct *tJKFAllReplyPointer = JKAllReplyPointer; #if defined(ENABLE_MONITORING) # if defined(MONOTORING_PERIOD_FAST) @@ -960,13 +1025,13 @@ void printJKDynamicInfo() { printCSVLine(); } # else - // print CSV line every minute and CSV caption every 10 minutes +// print CSV line every minute and CSV caption every 10 minutes if (sUpTimeStringMinuteHasChanged) { sUpTimeStringMinuteHasChanged = false; printCSVLine(); } # endif - // Print +CSV line every percent of nominal battery capacity (TotalCapacityAmpereHour) for capacity to voltage graph +// Print +CSV line every percent of nominal battery capacity (TotalCapacityAmpereHour) for capacity to voltage graph if (abs( JKLastPrintedData.BatteryCapacityAccumulator10MilliAmpere - JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere) > getOnePercentCapacityAsAccumulator10Milliampere()) { @@ -988,7 +1053,7 @@ void printJKDynamicInfo() { # endif Serial.println(); Serial.print(F("Total Runtime: ")); - Serial.print(swap(sJKFAllReplyPointer->SystemWorkingMinutes)); + Serial.print(swap(JKAllReplyPointer->SystemWorkingMinutes)); Serial.print(F(" minutes -> ")); Serial.println(sUpTimeString); @@ -1045,7 +1110,7 @@ void printJKDynamicInfo() { */ #if defined(LOCAL_DEBUG) Serial.print(F("TokenTemperaturePowerMosFet=0x")); - Serial.println(sJKFAllReplyPointer->TokenTemperaturePowerMosFet, HEX); + Serial.println(JKAllReplyPointer->TokenTemperaturePowerMosFet, HEX); #endif if (abs(JKComputedData.TemperaturePowerMosFet - JKLastPrintedData.TemperaturePowerMosFet) > 2 || abs(JKComputedData.TemperatureSensor1 - JKLastPrintedData.TemperatureSensor1) > 2 @@ -1135,12 +1200,12 @@ const char sCSVCaption[] PROGMEM * E.g. 13753;185;185;186;185;185;206;185;214;183;185;201;186;186;186;185;186;5096;-5.65;36;54;1 * */ -void setCSVString() { +void JK_BMS::setCSVString() { /* * Uptime minutes */ - uint_fast8_t tBufferIndex = sprintf_P(sStringBuffer, PSTR("%lu;"), (swap(sJKFAllReplyPointer->SystemWorkingMinutes))); + uint_fast8_t tBufferIndex = sprintf_P(sStringBuffer, PSTR("%lu;"), (swap(JKAllReplyPointer->SystemWorkingMinutes))); /* * Individual cell voltages @@ -1169,11 +1234,11 @@ void setCSVString() { sprintf_P(&sStringBuffer[tBufferIndex], PSTR("%u;%s;%ld;%d"), JKComputedData.BatteryVoltage10Millivolt * 10, tCurrentAsFloatString, JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere / (AMPERE_HOUR_AS_ACCUMULATOR_10_MILLIAMPERE / 10), /* 100mAh units*/ - sJKFAllReplyPointer->SOCPercent); + JKAllReplyPointer->SOCPercent); } } -void printCSVLine(char aLeadingChar) { +void JK_BMS::printCSVLine(char aLeadingChar) { if (aLeadingChar != '\0') { Serial.print(aLeadingChar); } diff --git a/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino b/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino index 529f86d..8d3963c 100644 --- a/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino +++ b/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino @@ -100,7 +100,7 @@ #include // For full revision history see https://github.com/ArminJo/JK-BMSToPylontechCAN?tab=readme-ov-file#revision-history -#define VERSION_EXAMPLE "3.2.1" +#define VERSION_EXAMPLE "4.0.0" /* * If battery SOC is below this value, the inverter is forced to charge the battery from any available power source regardless of inverter settings. @@ -122,7 +122,9 @@ //#define DEBUG // This enables debug output for all files - only for development //#define STANDALONE_TEST // If activated, fixed BMS data is sent to CAN bus and displayed on LCD. #if defined(STANDALONE_TEST) -//#define LCD_PAGES_TEST // Additional automatic tests +//#define ALARM_TIMEOUT_TEST +#define BEEP_TIMEOUT_SECONDS 20L // 20 beeps +//#define LCD_PAGES_TEST // LCD pages layout tests # if defined(LCD_PAGES_TEST) //#define BIG_NUMBER_TEST // Additional automatic tests, especially for rendering of JK_BMS_PAGE_BIG_INFO # endif @@ -214,13 +216,15 @@ char sStringBuffer[40]; // for "Store computed capacity" line, p */ #define BUZZER_PIN A2 // To signal errors #define PAGE_SWITCH_DEBUG_BUTTON_PIN_FOR_INFO 2 // Button at INT0 / D2 for switching LCD pages - definition is not used in program, only for documentation. -// The standard RX of the Arduino is used for the JK_BMS connection. +// The standard RX of the Arduino is used for the JK_BMS_1 connection. #define JK_BMS_RX_PIN_FOR_INFO 0 // We use the Serial RX pin - definition is not used in program, only for documentation. #if !defined(JK_BMS_TX_PIN) // Allow override by global symbol # if defined(USE_LAYOUT_FOR_644_BOARD) #define JK_BMS_TX_PIN 12 +#define JK_BMS_TX_PIN_2 12 // use same pin for tests # else #define JK_BMS_TX_PIN 4 +#define JK_BMS_TX_PIN_2 5 # endif #endif @@ -294,6 +298,8 @@ char sStringBuffer[40]; // for "Store computed capacity" line, p #define SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS "2" // Only for display on LCD #define SECONDS_BETWEEN_CAN_FRAME_SEND "2" // Only for display on LCD #endif +uint32_t sMillisOfLastRequestedJKDataFrame = -MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS; // Initial value to start first request immediately +bool sResponseFrameBytesAreExpected = false; // If true, request was recently sent /* * Error beep behavior @@ -302,13 +308,22 @@ char sStringBuffer[40]; // for "Store computed capacity" line, p */ //#define NO_BEEP_ON_ERROR // If activated, Do not beep on error or timeout. //#define ONE_BEEP_ON_ERROR // If activated, only beep once if error was detected. +//#define BEEP_TIMEOUT_SECONDS 10L // 10 seconds, Maximum is 254 seconds = 4 min 14 s #define SUPPRESS_CONSECUTIVE_SAME_ALARMS // If activated, recurring alarms are suppressed, e.g. undervoltage alarm tends to come and go in a minute period -#define BEEP_TIMEOUT_SECONDS 60L // 1 minute, Maximum is 254 seconds = 4 min 14 s +#if defined(SUPPRESS_CONSECUTIVE_SAME_ALARMS) +uint16_t sLastActiveAlarmsAsWord; // Used to disable new alarm beep on recurring of same alarm +bool sPrintAlarmInfoOnlyOnce = false; // This forces display on Alarm page +#define SUPPRESS_CONSECUTIVE_SAME_ALARMS_TIMEOUT_SECONDS 3600 // Allow consecutive same alarms after 1 hour of no alarm +uint16_t sNoConsecutiveAlarmTimeoutCounter; // Counts no alarms in order to reset suppression of consecutive alarms. +#endif #if !defined(NO_BEEP_ON_ERROR) && !defined(ONE_BEEP_ON_ERROR) #define MULTIPLE_BEEPS_WITH_TIMEOUT // If activated, beep for 1 minute if error was detected. Timeout is disabled if debug is active. #endif -bool sDoAlarmOrTimeoutBeep = false; // If true, we do an error beep at the end of the loop -# if defined(MULTIPLE_BEEPS_WITH_TIMEOUT) // Beep one minute +bool sAlarmOrTimeoutBeepActive = false; // If true, we do an error beep at each end of the loop until beep timeout +#if defined(MULTIPLE_BEEPS_WITH_TIMEOUT) // Beep one minute +# 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; #endif #if defined(NO_BEEP_ON_ERROR) && defined(ONE_BEEP_ON_ERROR) @@ -317,6 +332,16 @@ uint8_t sBeepTimeoutCounter = 0; #define MILLIS_IN_ONE_SECOND 1000L +/* + * Function declarations + */ +void printReceivedData(); +bool isVCCTooHighSimple(); +void handleOvervoltage(); +void handleBeep(); +void processJK_BMSStatusFrame(); +void handleFrameReceiveTimeout(); + /* * Page button stuff * @@ -334,41 +359,24 @@ volatile bool sPageButtonJustPressed = false; void handlePageButtonPress(bool aButtonToggleState); // The button press callback function sets just a flag. EasyButton PageSwitchButtonAtPin2(&handlePageButtonPress); // Button is connected to INT0 #define LONG_PRESS_BUTTON_DURATION_MILLIS 1000 -bool sDebugModeActivated = false; // Is activated on long press +bool sCommunicationDebugModeActivated = false; // Is true for JK_BMS_PAGE_CAN_INFO void checkButtonPress(); -bool readJK_BMSStatusFrame(); -void processJK_BMSStatusFrame(); -void handleFrameReceiveTimeout(); - -#include "HexDump.hpp" #include "digitalWriteFast.h" /* - * Software serial for JK-BMS stuff + * JK-BMS communication stuff */ #if !defined(MAXIMUM_NUMBER_OF_CELLS) #define MAXIMUM_NUMBER_OF_CELLS 24 // Maximum number of cell info which can be converted. Must be before #include "JK-BMS.hpp". #endif #include "JK-BMS.hpp" -/* - * Software serial for JK-BMS request frame sending - */ -#include "SoftwareSerialTX.h" -/* - * Use a 115200 baud software serial for the short request frame. - * If available, we also can use a second hardware serial here :-). - */ -SoftwareSerialTX TxToJKBMS(JK_BMS_TX_PIN); -bool sResponseFrameBytesAreExpected = false; // If true, request was recently sent so now check for serial input of response frame -uint32_t sMillisOfLastRequestedJKDataFrame = -MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS; // Initial value to start first request immediately -/* - * BMS communication timeout - */ -uint32_t sMillisOfLastReceivedByte = 0; // For timeout detection -bool sJKBMSFrameHasTimeout; // True, as long as BMS timeout persists. -bool sTimeoutJustdetected = false; // Is set to true at first detection of timeout and reset by beep timeout +JK_BMS JK_BMS_1; +//#define HANDLE_MULTIPLE_BMS +#if defined(HANDLE_MULTIPLE_BMS) +JK_BMS JK_BMS_2; +#endif /* * CAN stuff @@ -403,29 +411,16 @@ uint32_t sMillisOfLastCANFrameSent = 0; // For CAN timing /* * Optional LCD stuff */ -#if !defined(USE_NO_LCD) +#if defined(USE_SERIAL_2004_LCD) #include "JK-BMS_LCD.hpp" #endif -bool sBMSFrameProcessingComplete = false; // True if one status frame was received and processed or timeout happened. Then we can do a sleep at the end of the loop. - /* * Miscellaneous */ -#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 bool sInitialActionsPerformed = false; // Flag to send static info only once after reset. -void processReceivedData(); -void printReceivedData(); -bool isVCCTooHighSimple(); -void handleOvervoltage(); - -/* - * Must be after all .hpp includes - */ +// 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" // no include "LocalDebugLevelEnd.h" required :-) @@ -458,6 +453,7 @@ const uint8_t TestJKReplyStatusFrame[] PROGMEM = { /* Header*/0x4E, 0x57, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x51, 0xC2 }; void doStandaloneTest(); +void initializeEEPROMForTest(); #endif /* @@ -558,8 +554,13 @@ delay(4000); // To be able to connect Serial monitor after reset or power up and /* * 115200 baud soft serial to JK-BMS. For serial from BMS we use the hardware Serial RX. */ - TxToJKBMS.begin(115200); - Serial.println(F("Serial to JK-BMS started with 115.200 kbit/s!")); + JK_BMS_1.init(JK_BMS_TX_PIN); +#if defined(HANDLE_MULTIPLE_BMS) + JK_BMS_2.init(JK_BMS_TX_PIN_2); + Serial.println(F("Serial to JK-BMS 1 + 2 started with 115.200 kbit/s at pins " STR(JK_BMS_TX_PIN) " + " STR(JK_BMS_TX_PIN_2))); +#else + Serial.println (F("Serial to JK-BMS started with 115.200 kbit/s at pin " STR(JK_BMS_TX_PIN))); +#endif #if defined(USE_SERIAL_2004_LCD) if (sSerialLCDAvailable) { myLCD.setCursor(0, 2); @@ -625,124 +626,59 @@ delay(4000); // To be able to connect Serial monitor after reset or power up and Serial.println(F("Standalone test. Use fixed demo data")); Serial.println(); memcpy_P(JKReplyFrameBuffer, TestJKReplyStatusFrame, sizeof(TestJKReplyStatusFrame)); - sReplyFrameBufferIndex = sizeof(TestJKReplyStatusFrame) - 1; - printJKReplyFrameBuffer(); + JK_BMS_1.ReplyFrameBufferIndex = sizeof(TestJKReplyStatusFrame) - 1; + JK_BMS_1.printJKReplyFrameBuffer(); Serial.println(); - processReceivedData(); // sets sCANDataIsInitialized to true + sResponseFrameBytesAreExpected = false; // Enable CAN data sending + JK_BMS_1.processReceivedData(); // sets sCANDataIsInitialized to true printReceivedData(); /* * Copy complete reply and computed values for change determination */ - lastJKReply.SOCPercent = sJKFAllReplyPointer->SOCPercent; - lastJKReply.AlarmUnion.AlarmsAsWord = sJKFAllReplyPointer->AlarmUnion.AlarmsAsWord; - lastJKReply.BMSStatus.StatusAsWord = sJKFAllReplyPointer->BMSStatus.StatusAsWord; - lastJKReply.SystemWorkingMinutes = sJKFAllReplyPointer->SystemWorkingMinutes; + 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.BMSStatus.StatusAsWord = JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusAsWord; + JK_BMS_1.lastJKReply.SystemWorkingMinutes = JK_BMS_1.JKAllReplyPointer->SystemWorkingMinutes; // if (true) { // first value is still written by processReceivedData if (SOCDataPointsInfo.ArrayLength > 250) { Serial.println(F("EEPROM for standalone test is already initialized")); } else { - // If EEPROM empty, fill in some values - int8_t tIncrement = -1; - Serial.println(); - Serial.println(); - Serial.println(F("Now write initial data to EEPROM")); - Serial.print(F("SOC=")); - Serial.println(sJKFAllReplyPointer->SOCPercent); - Serial.print(F("JKComputedData.Battery10MilliAmpere=")); - Serial.println(JKComputedData.Battery10MilliAmpere); - Serial.print(F("JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt=")); - Serial.println(JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt); - - for (int i = 0; i < 260; ++i) { // force buffer wrap around - sJKFAllReplyPointer->SOCPercent += tIncrement; - writeSOCData(); // write too EEPROM here. - - // change accumulated value - JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt += tIncrement * 4; - - Serial.print(sJKFAllReplyPointer->SOCPercent); - Serial.print(F(", ")); - - // Test negative values - if (sJKFAllReplyPointer->SOCPercent == 2 && tIncrement < 0) { - JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt = -10; // -100 mV - Serial.print(F(" -100 mV ")); - } - - /* - * Generate EEPROM data by calling writeSOCData(); multiple times for each SOC value - */ - for (int j = 0; j < (i + 4) * 2; ++j) { // 320 corresponds to 2 Ah - // accumulate values - writeSOCData(); - lastJKReply.SOCPercent = sJKFAllReplyPointer->SOCPercent; // for transition detection from 0 to 1 - } - - // Manage 0% and 100 % - if (sJKFAllReplyPointer->SOCPercent == 0 || sJKFAllReplyPointer->SOCPercent == 100) { - if (sJKFAllReplyPointer->SOCPercent == 100) { - // Add additional capacity after 100% - for (int j = 0; j < 8000; ++j) { - writeSOCData(); - } - } else { - // set difference voltage to 0 - JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt = 0; - } - // reverse values at 0 and 100 - JKComputedData.Battery10MilliAmpere = -JKComputedData.Battery10MilliAmpere; - tIncrement = -tIncrement; - } - } - Serial.println(); - findFirstSOCDataPointIndex(); - Serial.print(F("EEPROM SOC data start index=")); - Serial.print(SOCDataPointsInfo.ArrayStartIndex); - Serial.print(F(" length=")); - Serial.print(SOCDataPointsInfo.ArrayLength); - Serial.print(F(", even=")); - Serial.println(SOCDataPointsInfo.currentlyWritingOnAnEvenPage); + initializeEEPROMForTest(); } doStandaloneTest(); #endif } +#if defined(HANDLE_MULTIPLE_BMS) +JK_BMS *sCurrentBMS = &JK_BMS_1; +#else +JK_BMS *const sCurrentBMS = &JK_BMS_1; +#endif void loop() { checkButtonPress(); +#if defined(LOCAL_DEBUG) + sCommunicationDebugModeActivated = true; +#endif /* - * Request status frame every 2 seconds + * Request status frame every 2 seconds and check for response */ if (millis() - sMillisOfLastRequestedJKDataFrame >= MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) { sMillisOfLastRequestedJKDataFrame = millis(); // set for next check - /* - * Flush input buffer and send request to JK-BMS - */ - while (Serial.available()) { - Serial.read(); - } -#if defined(TIMING_TEST) - digitalWriteFast(TIMING_TEST_PIN, HIGH); -#endif - requestJK_BMSStatusFrame(&TxToJKBMS, sDebugModeActivated); // 1.85 ms -#if defined(TIMING_TEST) - digitalWriteFast(TIMING_TEST_PIN, LOW); -#endif + sCurrentBMS->RequestStatusFrame(sCommunicationDebugModeActivated); sResponseFrameBytesAreExpected = true; // Enable check for serial input of response - initJKReplyFrameBuffer(); - sMillisOfLastReceivedByte = millis(); // initialize reply timeout } #if defined(STANDALONE_TEST) - sResponseFrameBytesAreExpected = false; // No response! - sBMSFrameProcessingComplete = true; // for LCD timeout etc. - processReceivedData(); // for statistics + JK_BMS_1.processReceivedData(); // call it multiple times for statistics writeSOCData(); // for analytics tests printBMSDataOnLCD(); // for switching between MAX and MIN display + fillAllCANData(&JK_BMS_1); + sCANDataIsInitialized = true; // One time flag delay(MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS); // do it simple :-) # if !defined(USE_NO_COMMUNICATION_STATUS_LEDS) @@ -753,46 +689,93 @@ void loop() { delay(20); // do it simple :-) # endif -#else - /* - * Get reply from BMS and check timeout - */ - if (sResponseFrameBytesAreExpected) { - if (Serial.available()) { -# if defined(TIMING_TEST) - digitalWriteFast(TIMING_TEST_PIN, HIGH); +# 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 - if (readJK_BMSStatusFrame()) { +#else // defined(STANDALONE_TEST) + + if (sResponseFrameBytesAreExpected) { + uint8_t tBMSCommunicationReturnCode = sCurrentBMS->checkForReplyFromBMSOrTimeout(); + /* + * Check for reply from BMS or timeout + */ + if (tBMSCommunicationReturnCode != JK_BMS_RECEIVE_ONGOING) { + sResponseFrameBytesAreExpected = false; + + if (sCurrentBMS->TimeoutJustDetected) { + handleFrameReceiveTimeout(); + } + + /* + * Process BMS reply + */ + if (tBMSCommunicationReturnCode == JK_BMS_RECEIVE_FINISHED) { + TURN_BMS_STATUS_LED_ON; /* - * Frame completely received, now process it + * First compute some internal variables */ - sBMSFrameProcessingComplete = true; - TURN_BMS_STATUS_LED_ON; - processJK_BMSStatusFrame(); // Process the complete receiving of the status frame and set the appropriate flags + sCurrentBMS->processReceivedData(); + processJK_BMSStatusFrame(); // Process the complete receiving of the status frame + +# if defined(HANDLE_MULTIPLE_BMS) + if (sCurrentBMS->NumberOfBMS == 1) { + TURN_BMS_STATUS_LED_OFF; + // Process 2. BMS + delay(MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS / 2); // a hack + sCurrentBMS = &JK_BMS_2; + sCurrentBMS->RequestStatusFrame(sCommunicationDebugModeActivated); + sResponseFrameBytesAreExpected = true; // Enable check for serial input of response + + } else { + /* + * Fill CAN data after both BMS are processed + */ + fillAllCANData(sCurrentBMS); + sCANDataIsInitialized = true; // One time flag + + sCurrentBMS = &JK_BMS_1; // switch back to first BMS + } +# else + /* + * Fill CAN data + */ + fillAllCANData(sCurrentBMS); + sCANDataIsInitialized = true; // One time flag + + processJK_BMSStatusFrame();// Process the complete receiving of the status frame + +# endif TURN_BMS_STATUS_LED_OFF; + sCommunicationDebugModeActivated = false; // Reset flag here. } -# if defined(TIMING_TEST) - digitalWriteFast(TIMING_TEST_PIN, LOW); -# endif - } else if (millis() - sMillisOfLastReceivedByte >= TIMEOUT_MILLIS_FOR_FRAME_REPLY) { + /******************************************************************* + * Do this once after each complete status frame or timeout or error + *******************************************************************/ /* - * Here we have requested response frame, but serial was not available fore a longer time => timeout at receiving + * Check for overvoltage */ - sBMSFrameProcessingComplete = true; // Do frame complete handling like beep anyway - sResponseFrameBytesAreExpected = false; // Do not try to receive more response bytes + while (isVCCTooHighSimple()) { + handleOvervoltage(); + } - if (!sJKBMSFrameHasTimeout) { - /* - * Do this only once per timeout - */ - sJKBMSFrameHasTimeout = true; - sTimeoutJustdetected = true; // This forces the beep - handleFrameReceiveTimeout(); +# 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) +#endif // defined(STANDALONE_TEST) /* * Send CAN frame independently of the period of JK-BMS data requests, @@ -805,173 +788,155 @@ void loop() { Serial.print(F("sCANDataIsInitialized=")); Serial.print(sCANDataIsInitialized); Serial.print(F(" BMSIsStarting=")); - Serial.print(JKComputedData.BMSIsStarting); + Serial.print(sCurrentBMS->JKComputedData.BMSIsStarting); Serial.print(F(", sResponseFrameBytesAreExpected=")); Serial.println(sResponseFrameBytesAreExpected); #endif - if (sCANDataIsInitialized && !JKComputedData.BMSIsStarting && !sResponseFrameBytesAreExpected + if (sCANDataIsInitialized && !sCurrentBMS->JKComputedData.BMSIsStarting && !sResponseFrameBytesAreExpected && millis() - sMillisOfLastCANFrameSent >= MILLISECONDS_BETWEEN_CAN_FRAME_SEND) { sMillisOfLastCANFrameSent = millis(); TURN_CAN_STATUS_LED_ON; - if (sDebugModeActivated) { + if (sCommunicationDebugModeActivated) { Serial.println(F("Send CAN")); } - sendAllCANFrames(sDebugModeActivated); + sendAllCANFrames(sCommunicationDebugModeActivated); TURN_CAN_STATUS_LED_OFF; } +} - /********************************************************** - * Do this once after each complete status frame or timeout - **********************************************************/ - if (sBMSFrameProcessingComplete) { - sDebugModeActivated = false; // reset flag here. It may be set again at start of next loop. +void handleBeep() { + bool tDoBeep = false; +#if defined(SUPPRESS_CONSECUTIVE_SAME_ALARMS) + if (!sCurrentBMS->AlarmActive) { /* - * Check for overvoltage + * Check for 1 hour of no alarm, then reset suppression of same alarm */ - while (isVCCTooHighSimple()) { - handleOvervoltage(); + 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) { + sLastActiveAlarmsAsWord = NO_ALARM_WORD_CONTENT; // Reset LastActiveAlarmsAsWord } + } -#if defined(USE_SERIAL_2004_LCD) && !defined(DISPLAY_ALWAYS_ON) - if (sSerialLCDAvailable) { - doLCDBacklightTimeoutHandling(); + if (sCurrentBMS->AlarmJustGetsActive) { + sNoConsecutiveAlarmTimeoutCounter = 0; // initialize timeout counter + // If new alarm is equal old one, beep only once + if (sLastActiveAlarmsAsWord == sCurrentBMS->JKAllReplyPointer->AlarmUnion.AlarmsAsWord) { + // do only one beep for recurring alarms + sCurrentBMS->AlarmJustGetsActive = false; // avoid evaluation below + tDoBeep = true; // Beep only once in this loop + } else { + /* + * Here at least one alarm is active and it is NOT the last alarm processed + */ + sLastActiveAlarmsAsWord = sCurrentBMS->JKAllReplyPointer->AlarmUnion.AlarmsAsWord; } + } +#endif + /* + * Check for BMS alarm flags + */ + if (sCurrentBMS->AlarmJustGetsActive || sCurrentBMS->TimeoutJustDetected) { + sCurrentBMS->AlarmJustGetsActive = false; + sCurrentBMS->TimeoutJustDetected = false; +#if defined(ONE_BEEP_ON_ERROR) + tDoBeep = true; // Beep only once in this loop +#else + sAlarmOrTimeoutBeepActive = true; // enable beep until beep timeout #endif - - /* - * Check for BMS alarm flags - */ - if (sAlarmJustGetsActive || sTimeoutJustdetected) { - sDoAlarmOrTimeoutBeep = true; #if defined(MULTIPLE_BEEPS_WITH_TIMEOUT) - } else { - sBeepTimeoutCounter = 0; + sBeepTimeoutCounter = 0; #endif - } - - /* - * Alarm / beep handling - */ -#if !defined(NO_BEEP_ON_ERROR) // Beep enabled - -# if defined(ONE_BEEP_ON_ERROR) // Beep once - if (sDoAlarmOrTimeoutBeep) { - sAlarmJustGetsActive = false; // disable further alarm beeps - sTimeoutJustdetected = false; // disable further timeout beeps - } -# endif + } + if (!(sCurrentBMS->AlarmActive || sCurrentBMS->JKBMSFrameHasTimeout)) { + sAlarmOrTimeoutBeepActive = false; // No more error, stop beeping + } - if (sDoAlarmOrTimeoutBeep) { - sDoAlarmOrTimeoutBeep = false; -# 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) { - JK_INFO_PRINTLN(F("Timeout reached, suppress consecutive error beeps")); - sAlarmJustGetsActive = false; // disable further alarm beeps - sTimeoutJustdetected = false; // disable further timeout beeps - } else -# endif - { - tone(BUZZER_PIN, 2200); - tone(BUZZER_PIN, 2200, 50); - delay(200); - tone(BUZZER_PIN, 2200, 50); - delay(200); // to avoid tone interrupts waking us up from sleep - } + /* + * Is beep requested for this loop ? + */ +#if !defined(ONE_BEEP_ON_ERROR) + if (sAlarmOrTimeoutBeepActive) { + 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) { + JK_INFO_PRINTLN(F("Timeout reached, suppress consecutive error beeps")); + sAlarmOrTimeoutBeepActive = false; // disable further alarm beeps + } else +#endif + { + tone(BUZZER_PIN, 2200); + tone(BUZZER_PIN, 2200, 50); + delay(200); + tone(BUZZER_PIN, 2200, 50); + delay(200); // to avoid tone interrupts waking us up from sleep } -#endif // NO_BEEP_ON_ERROR - - sBMSFrameProcessingComplete = false; // prepare for next loop - - } // if (sBMSFrameProcessingComplete) + } } /* - * Process the complete receiving of the status frame and set the appropriate flags + * Process the complete receiving of the status frame + * Print data on LCD and Serial + * Do initial actions like printing static values after boot + * Print received buffer, if debug enabled */ void processJK_BMSStatusFrame() { - if (sDebugModeActivated) { + if (sCommunicationDebugModeActivated) { /* - * Do it once at every debug start + * Print received buffer, if debug enabled */ - if (sReplyFrameBufferIndex == 0) { + if (sCurrentBMS->ReplyFrameBufferIndex == 0) { Serial.println(F("sReplyFrameBufferIndex is 0")); } else { - Serial.print(sReplyFrameBufferIndex + 1); - Serial.println(F(" bytes received")); - printJKReplyFrameBuffer(); + sCurrentBMS->printJKReplyFrameBuffer(); } - Serial.println(); - } - - if (sJKBMSFrameHasTimeout) { - // First response frame after timeout :-) - sJKBMSFrameHasTimeout = false; - sTimeoutJustdetected = false; -#if defined(USE_SERIAL_2004_LCD) && !defined(DISPLAY_ALWAYS_ON) - checkAndTurnLCDOn(); // Reason is printed anyway :-) -#endif - JK_INFO_PRINTLN(F("Successfully receiving first BMS status frame after BMS communication timeout")); } - processReceivedData(); if (!sInitialActionsPerformed) { /* * Do initialization once here */ +# if defined(HANDLE_MULTIPLE_BMS) + // Print static info for both BMS + if (sCurrentBMS->NumberOfBMS == 2) { + sInitialActionsPerformed = true; + } +# else sInitialActionsPerformed = true; - initializeComputedData(); + +# endif Serial.println(); - printJKStaticInfo(); + sCurrentBMS->printJKStaticInfo(); #if !defined(NO_ANALYTICS) +# if defined(HANDLE_MULTIPLE_BMS) + if (sCurrentBMS->NumberOfBMS == 1) { + initializeAnalytics(); + } +# else initializeAnalytics(); + +# endif JK_INFO_PRINTLN(); #endif } printReceivedData(); #if !defined(NO_ANALYTICS) +# if defined(HANDLE_MULTIPLE_BMS) + if (sCurrentBMS->NumberOfBMS == 1) { + writeSOCData(); + } +# else writeSOCData(); +# endif #endif - /* - * Copy complete reply and computed values for change determination - */ - lastJKReply.SOCPercent = sJKFAllReplyPointer->SOCPercent; - lastJKReply.AlarmUnion.AlarmsAsWord = sJKFAllReplyPointer->AlarmUnion.AlarmsAsWord; - lastJKReply.BMSStatus.StatusAsWord = sJKFAllReplyPointer->BMSStatus.StatusAsWord; - lastJKReply.SystemWorkingMinutes = sJKFAllReplyPointer->SystemWorkingMinutes; -} - -/* - * Reads all bytes of the requested frame into buffer and prints errors - * Sets sResponseFrameBytesAreExpected to false, if frame was complete and manages other flags too - * @return true if frame was completely received - */ -bool readJK_BMSStatusFrame() { - sMillisOfLastReceivedByte = millis(); - uint8_t tReceiveResultCode = readJK_BMSStatusFrameByte(); - if (tReceiveResultCode == JK_BMS_RECEIVE_FINISHED) { - /* - * All JK-BMS status frame data received - */ - sResponseFrameBytesAreExpected = false; // Do not try to receive more response bytes - return true; - - } else if (tReceiveResultCode != JK_BMS_RECEIVE_OK) { - /* - * Error here - */ - Serial.print(F("Receive error=")); - Serial.print(tReceiveResultCode); - Serial.print(F(" at index")); - Serial.println(sReplyFrameBufferIndex); - sResponseFrameBytesAreExpected = false; // Do not try to receive more response bytes - sBMSFrameProcessingComplete = true; // In case of error, we do not call processJK_BMSStatusFrame(), so set flag here. - printJKReplyFrameBuffer(); - } - return false; } /* @@ -980,13 +945,17 @@ bool readJK_BMSStatusFrame() { */ void handleFrameReceiveTimeout() { Serial.print(F("Receive timeout")); +#if defined(HANDLE_MULTIPLE_BMS) + Serial.print(F(" at BMS ")); + Serial.print(sCurrentBMS->NumberOfBMS); +#endif - if (sReplyFrameBufferIndex != 0) { + if (sCurrentBMS->ReplyFrameBufferIndex != 0) { // Print bytes received so far Serial.print(F(" at ReplyFrameBufferIndex=")); - Serial.print(sReplyFrameBufferIndex); - if (sReplyFrameBufferIndex != 0) { - printJKReplyFrameBuffer(); + Serial.print(sCurrentBMS->ReplyFrameBufferIndex); + if (sCurrentBMS->ReplyFrameBufferIndex != 0) { + sCurrentBMS->printJKReplyFrameBuffer(); } } Serial.println(); @@ -1008,30 +977,6 @@ void handleFrameReceiveTimeout() { #endif } -void processReceivedData() { - /* - * Set the static pointer to the start of the reply data which depends on the number of cell voltage entries - * The JKFrameAllDataStruct starts behind the header + cell data header 0x79 + CellInfoSize + the variable length cell data 3 bytes per cell, (CellInfoSize is contained in JKReplyFrameBuffer[12]) - */ - sJKFAllReplyPointer = reinterpret_cast(&JKReplyFrameBuffer[JK_BMS_FRAME_HEADER_LENGTH + 2 - + JKReplyFrameBuffer[JK_BMS_FRAME_INDEX_OF_CELL_INFO_LENGTH]]); - - fillJKConvertedCellInfo(); - /* - * Print newline, if SOC changed - */ - if (sJKFAllReplyPointer->SOCPercent != lastJKReply.SOCPercent) { - Serial.println(); - } - fillJKComputedData(); - - computeUpTimeString(); - detectAndPrintAlarmInfo(); // UpTimeString is used here - - fillAllCANData(sJKFAllReplyPointer); - sCANDataIsInitialized = true; // One time flag -} - /* * Called exclusively by processJK_BMSStatusFrame() */ @@ -1039,11 +984,11 @@ void printReceivedData() { #if defined(USE_SERIAL_2004_LCD) if (sLCDDisplayPageNumber != JK_BMS_PAGE_CAPACITY_INFO) { // Do not interfere with plotter output - printJKDynamicInfo(); // Prints newline before SOC[%]= + sCurrentBMS->printJKDynamicInfo(); // Prints newline before SOC[%]= } printBMSDataOnLCD(); #else - printJKDynamicInfo(); + sCurrentBMS->printJKDynamicInfo(); #endif } @@ -1067,18 +1012,104 @@ void checkButtonPress() { */ if (sPageButtonJustPressed) { sPageButtonJustPressed = false; - sDebugModeActivated = true; // Is set to false in loop + sCommunicationDebugModeActivated = true; // Is set to false in loop Serial.println(F("One time debug print just activated")); } else if (PageSwitchButtonAtPin2.readDebouncedButtonState()) { // Button is still pressed - sDebugModeActivated = true; // Is set to false in loop + sCommunicationDebugModeActivated = true; // Is set to false in loop } #endif // defined(USE_SERIAL_2004_LCD) } #if defined(STANDALONE_TEST) +void initializeEEPROMForTest() { + // If EEPROM empty, fill in some values + int8_t tIncrement = -1; + Serial.println(); + Serial.println(); + Serial.println(F("Now write initial data to EEPROM")); + Serial.print(F("SOC=")); + Serial.println(JK_BMS_1.JKAllReplyPointer->SOCPercent); + Serial.print(F("JKComputedData.Battery10MilliAmpere=")); + Serial.println(JK_BMS_1.JKComputedData.Battery10MilliAmpere); + Serial.print(F("JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt=")); + Serial.println(JK_BMS_1.JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt); + + for (int i = 0; i < 260; ++i) { // force buffer wrap around + JK_BMS_1.JKAllReplyPointer->SOCPercent += tIncrement; + writeSOCData(); // write too EEPROM here. + + // change accumulated value + JK_BMS_1.JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt += tIncrement * 4; + + Serial.print(JK_BMS_1.JKAllReplyPointer->SOCPercent); + Serial.print(F(", ")); + + // Test negative values + if (JK_BMS_1.JKAllReplyPointer->SOCPercent == 2 && tIncrement < 0) { + JK_BMS_1.JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt = -10; // -100 mV + Serial.print(F(" -100 mV ")); + } + + /* + * Generate EEPROM data by calling writeSOCData(); multiple times for each SOC value + */ + for (int j = 0; j < (i + 4) * 2; ++j) { // 320 corresponds to 2 Ah + // accumulate values + writeSOCData(); + JK_BMS_1.lastJKReply.SOCPercent = JK_BMS_1.JKAllReplyPointer->SOCPercent; // for transition detection from 0 to 1 + } + + // Manage 0% and 100 % + if (JK_BMS_1.JKAllReplyPointer->SOCPercent == 0 || JK_BMS_1.JKAllReplyPointer->SOCPercent == 100) { + if (JK_BMS_1.JKAllReplyPointer->SOCPercent == 100) { + // Add additional capacity after 100% + for (int j = 0; j < 8000; ++j) { + writeSOCData(); + } + } else { + // set difference voltage to 0 + JK_BMS_1.JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt = 0; + } + // reverse values at 0 and 100 + JK_BMS_1.JKComputedData.Battery10MilliAmpere = -JK_BMS_1.JKComputedData.Battery10MilliAmpere; + tIncrement = -tIncrement; + } + } + Serial.println(); + findFirstSOCDataPointIndex(); + Serial.print(F("EEPROM SOC data start index=")); + Serial.print(SOCDataPointsInfo.ArrayStartIndex); + Serial.print(F(" length=")); + Serial.print(SOCDataPointsInfo.ArrayLength); + Serial.print(F(", even=")); + Serial.println(SOCDataPointsInfo.currentlyWritingOnAnEvenPage); +} + +/* + * Assume, that MULTIPLE_BEEPS_WITH_TIMEOUT is active + */ +void testAlarmTimeout() { + Serial.println(F("Test alarm timeout")); + JK_BMS_1.JKAllReplyPointer->AlarmUnion.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) { + handleBeep(); + delay(100); + } + // reset alarm + JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm = false; + JK_BMS_1.detectAndPrintAlarmInfo(); // this sets the LCD alarm string and flags + Serial.println(F("End of test alarm timeout")); + +} + void doStandaloneTest() { +# if defined(ALARM_TIMEOUT_TEST) + testAlarmTimeout(); +#endif # if defined(LCD_PAGES_TEST) if (sSerialLCDAvailable) { testLCDPages(); diff --git a/JK-BMSToPylontechCAN/JK-BMS_Analytics.hpp b/JK-BMSToPylontechCAN/JK-BMS_Analytics.hpp index b26a996..8a732cc 100644 --- a/JK-BMSToPylontechCAN/JK-BMS_Analytics.hpp +++ b/JK-BMSToPylontechCAN/JK-BMS_Analytics.hpp @@ -38,6 +38,7 @@ #include "JK-BMS_Analytics.h" +// 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 //#define LOCAL_TRACE // This enables trace output only for this file - only for development #include "LocalDebugLevelStart.h" @@ -68,7 +69,7 @@ uint16_t computeSOCDataPointsInfoChecksum() { do { tChecksum += *tPointer++; } while (tPointer < &SOCDataPointsInfo.checksumForReboot); - DEBUG_PRINT(F("CS=")); + DEBUG_PRINT(F("SOCDataPointsInfoChecksum=")); DEBUG_PRINTLN(SOCDataPointsInfo.checksumForReboot); return tChecksum; } @@ -89,11 +90,11 @@ void initializeAnalytics() { memset(&SOCDataPointsInfo.NumberOfSamples, 0, 14); // Reset to computed value based on SOCPercent and TotalCapacityAmpereHour SOCDataPointsInfo.lastWrittenBatteryCapacityAsAccumulator10Milliampere = - JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere; + JK_BMS_1.JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere; JK_INFO_PRINT(tComputedChecksum); JK_INFO_PRINT(F(" != ")); JK_INFO_PRINT(SOCDataPointsInfo.checksumForReboot); - JK_INFO_PRINT(F(" -> clear")); + JK_INFO_PRINT(F(" -> Break detected: clear")); } else { JK_INFO_PRINT(F("Reboot detected: keep")); } @@ -102,7 +103,7 @@ void initializeAnalytics() { #else // Reset to computed value based on SOCPercent and TotalCapacityAmpereHour SOCDataPointsInfo.lastWrittenBatteryCapacityAsAccumulator10Milliampere = - JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere; + JK_BMS_1.JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere; #endif } /* @@ -137,6 +138,8 @@ void findFirstSOCDataPointIndex() { uint16_t tSOCDataPointsArrayLength = NUMBER_OF_SOC_DATA_POINTS; // value if SOC jump was found bool tStartPageIsEvenFlag; + DEBUG_PRINTLN(F("Search for even/odd toggling")); + for (uint16_t i = 0; i < NUMBER_OF_SOC_DATA_POINTS - 1; ++i) { uint8_t tSOCPercent = eeprom_read_byte(&SOCDataPointsEEPROMArray[i].SOCPercent); DEBUG_PRINT(F("tSOCPercent=0x")); @@ -270,7 +273,7 @@ void readAndPrintSOCData() { if (i == 0) { // Initialize start value of capacity with a reasonable value - tCurrentCapacity100MilliampereHour = (tCurrentSOCDataPoint.SOCPercent * JKComputedData.TotalCapacityAmpereHour) + tCurrentCapacity100MilliampereHour = (tCurrentSOCDataPoint.SOCPercent * JK_BMS_1.JKComputedData.TotalCapacityAmpereHour) / 10; tCurrentCapacityAmpereHour = tCurrentCapacity100MilliampereHour / 10; } @@ -279,7 +282,7 @@ void readAndPrintSOCData() { * Check for transition from 1 to 2 in order to reset capacity (before adding the delta) to the value of 1% */ if (tLastSOCPercent == 1 && tCurrentSOCDataPoint.SOCPercent == 2) { - tCurrentCapacity100MilliampereHour = JKComputedData.TotalCapacityAmpereHour / 10; // * 10 / 100 + tCurrentCapacity100MilliampereHour = JK_BMS_1.JKComputedData.TotalCapacityAmpereHour / 10; // * 10 / 100 tMinimumSOCData.CapacityAmpereHour = 0; // Set minimum to 0 } tLastSOCPercent = tCurrentSOCDataPoint.SOCPercent; @@ -424,9 +427,9 @@ void readAndPrintSOCData() { } Serial.print(F(" Empty_voltage_")); - Serial.print(JKComputedData.BatteryEmptyVoltage10Millivolt / 100.0, 1); + Serial.print(JK_BMS_1.JKComputedData.BatteryEmptyVoltage10Millivolt / 100.0, 1); Serial.print('_'); - Serial.print(swap(sJKFAllReplyPointer->CellUndervoltageProtectionMillivolt) / 1000.0, 2); + Serial.print(swap(JK_BMS_1.JKAllReplyPointer->CellUndervoltageProtectionMillivolt) / 1000.0, 2); Serial.println(F("V:0 _:0 _:0 _:0 _:0 _:0 _:0 _:0 _:0")); } @@ -493,27 +496,27 @@ void writeSOCData() { * This can happen at the start of the system, when there was no full 0% to 100% capacity cycle * and the BMS has not yet learned the correct capacity for 100%. */ - SOCDataPointsInfo.DeltaAccumulator10Milliampere += JKComputedData.Battery10MilliAmpere; // Can hold values of +/-11930 Ah - SOCDataPointsInfo.AverageAccumulator10Milliampere += JKComputedData.Battery10MilliAmpere; + SOCDataPointsInfo.DeltaAccumulator10Milliampere += JK_BMS_1.JKComputedData.Battery10MilliAmpere; // Can hold values of +/-11930 Ah + SOCDataPointsInfo.AverageAccumulator10Milliampere += JK_BMS_1.JKComputedData.Battery10MilliAmpere; SOCDataPointsInfo.AverageAccumulatorVoltageDifferenceToEmpty10Millivolt += - JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt; + JK_BMS_1.JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt; SOCDataPointsInfo.NumberOfSamples++; // For one sample each 2 seconds, we can store up to 36.4 hours here. TRACE_PRINT(F("#Samples=")); TRACE_PRINT(SOCDataPointsInfo.NumberOfSamples); TRACE_PRINT(F(", 10mA=")); - TRACE_PRINT(JKComputedData.Battery10MilliAmpere); + TRACE_PRINT(JK_BMS_1.JKComputedData.Battery10MilliAmpere); TRACE_PRINT(F(", DeltaAcc10mA=")); TRACE_PRINTLN(SOCDataPointsInfo.DeltaAccumulator10Milliampere); - auto tCurrentSOCPercent = sJKFAllReplyPointer->SOCPercent; + auto tCurrentSOCPercent = JK_BMS_1.JKAllReplyPointer->SOCPercent; /* * Check for transition from 0 to 1, where we can reset residual capacity */ - if (lastJKReply.SOCPercent == 0 && tCurrentSOCPercent == 1) { + if (JK_BMS_1.lastJKReply.SOCPercent == 0 && tCurrentSOCPercent == 1) { // Serial.println(F("SOC 0 -> 1 -> reset residual capacity")); SOCDataPointsInfo.DeltaAccumulator10Milliampere = 0; // Reset residual capacity SOCDataPointsInfo.lastWrittenBatteryCapacityAsAccumulator10Milliampere = - JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere; // Reset to computed value based on SOCPercent and TotalCapacityAmpereHour + JK_BMS_1.JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere; // Reset to computed value based on SOCPercent and TotalCapacityAmpereHour } uint16_t tSOCDataPointsArrayNextWriteIndex = (SOCDataPointsInfo.ArrayStartIndex + SOCDataPointsInfo.ArrayLength) @@ -537,8 +540,8 @@ void writeSOCData() { || (tLastWrittenSOCPercent == 1 && tCurrentSOCPercent != 1)) && abs( SOCDataPointsInfo.lastWrittenBatteryCapacityAsAccumulator10Milliampere - - JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere) - > getOnePercentCapacityAsAccumulator10Milliampere(); + - JK_BMS_1.JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere) + > JK_BMS_1.getOnePercentCapacityAsAccumulator10Milliampere(); if (tExtraCapacityChangedMoreThan1Percent) { JK_INFO_PRINTLN(F("Write data for extra capacity")); @@ -652,10 +655,10 @@ void writeSOCData() { SOCDataPointsInfo.AverageAccumulatorVoltageDifferenceToEmpty10Millivolt = 0; SOCDataPointsInfo.NumberOfSamples = 0; SOCDataPointsInfo.lastWrittenBatteryCapacityAsAccumulator10Milliampere = - JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere; + JK_BMS_1.JKComputedData.BatteryCapacityAsAccumulator10MilliAmpere; #if defined(ENABLE_MONITORING) // Special monitoring output to generate capacity to cell voltage graphs e.g. with excel - printCSVLine('#'); + JK_BMS_1.printCSVLine('#'); Serial.println(); #endif } diff --git a/JK-BMSToPylontechCAN/JK-BMS_LCD.h b/JK-BMSToPylontechCAN/JK-BMS_LCD.h index 94c72ec..3427190 100644 --- a/JK-BMSToPylontechCAN/JK-BMS_LCD.h +++ b/JK-BMSToPylontechCAN/JK-BMS_LCD.h @@ -1,7 +1,7 @@ /* - * JK-BMS_LCD.hpp + * JK-BMS_LCD.h * - * Contains LCD related variables and functions + * Contains declarations for LCD related variables and functions * * Copyright (C) 2023-2024 Armin Joachimsmeyer * Email: armin.joachimsmeyer@gmail.com diff --git a/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp b/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp index f374873..6bafaa7 100644 --- a/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp +++ b/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp @@ -35,7 +35,6 @@ bool sSerialLCDAvailable; char sStringBuffer[LCD_COLUMNS + 1]; // Only for rendering a LCD row with sprintf_P() #endif - //#define DISPLAY_ALWAYS_ON // Activate this, if you want the display to be always on. # if !defined(DISPLAY_ALWAYS_ON) void doLCDBacklightTimeoutHandling(); @@ -64,6 +63,12 @@ uint32_t sLastPageChangeMillis; uint8_t sLCDDisplayPageNumber = JK_BMS_START_PAGE; // Start with Big Info page uint8_t sToggleDisplayCounter; // counter for cell statistics page to determine max or min page and for capacity page +/* + * Since over and undervoltage is indicated by O or U in state info, it is not necessary to switch to Overview page + */ +//#define ENABLE_OVER_AND_UNDER_VOLTAGE_WARNING_ON_LCD // Enables switching to Overview page and showing over- and undervoltage data. Does not suppress the beeps for it! +bool sShowAlarmInsteadOfOverview = false; // is reset on button press + void setupLCD() { /* @@ -169,17 +174,17 @@ void doLCDBacklightTimeoutHandling() { # endif void printShortEnableFlagsOnLCD() { - if (sJKFAllReplyPointer->ChargeIsEnabled) { + if (JK_BMS_1.JKAllReplyPointer->ChargeIsEnabled) { myLCD.print('C'); } else { myLCD.print(' '); } - if (sJKFAllReplyPointer->ChargeIsEnabled) { + if (JK_BMS_1.JKAllReplyPointer->ChargeIsEnabled) { myLCD.print('D'); } else { myLCD.print(' '); } - if (sJKFAllReplyPointer->BalancingIsEnabled) { + if (JK_BMS_1.JKAllReplyPointer->BalancingIsEnabled) { myLCD.print('B'); } } @@ -191,23 +196,23 @@ void printShortEnableFlagsOnLCD() { */ void printShortStateOnLCD() { - if (sJKFAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm) { + if (JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm) { myLCD.print('O'); - } else if (sJKFAllReplyPointer->BMSStatus.StatusBits.ChargeMosFetActive) { + } else if (JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusBits.ChargeMosFetActive) { myLCD.print('C'); } else { myLCD.print(' '); } - if (sJKFAllReplyPointer->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm) { + if (JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm) { myLCD.print('U'); - } else if (sJKFAllReplyPointer->BMSStatus.StatusBits.DischargeMosFetActive) { + } else if (JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusBits.DischargeMosFetActive) { myLCD.print('D'); } else { myLCD.print(' '); } - if (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { + if (JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { myLCD.print('B'); } else { myLCD.print(' '); @@ -215,19 +220,19 @@ void printShortStateOnLCD() { } void printLongStateOnLCD() { - if (sJKFAllReplyPointer->BMSStatus.StatusBits.ChargeMosFetActive) { + if (JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusBits.ChargeMosFetActive) { myLCD.print(F("CH ")); } else { myLCD.print(F(" ")); } - if (sJKFAllReplyPointer->BMSStatus.StatusBits.DischargeMosFetActive) { + if (JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusBits.DischargeMosFetActive) { myLCD.print(F("DC ")); } else { myLCD.print(F(" ")); } - if (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { + if (JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { myLCD.print(F("BAL")); } } @@ -237,13 +242,13 @@ void printLongStateOnLCD() { * or state, if no alarm or under or overvoltage alarm, which is printed also in state */ void printAlarmHexOrStateOnLCD() { - if ((sJKFAllReplyPointer->AlarmUnion.AlarmsAsWord + if ((JK_BMS_1.JKAllReplyPointer->AlarmUnion.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(sJKFAllReplyPointer->AlarmUnion.AlarmsAsWord); + uint16_t tAlarms = swap(JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmsAsWord); if (tAlarms < 0x100) { myLCD.print(F("0x")); if (tAlarms < 0x10) { @@ -266,7 +271,7 @@ void printAlarmHexOrStateOnLCD() { */ void printCellInfoOnLCD() { uint_fast8_t tRowNumber; - auto tNumberOfCellInfoEntries = JKConvertedCellInfo.ActualNumberOfCellInfoEntries; + auto tNumberOfCellInfoEntries = JK_BMS_1.JKConvertedCellInfo.ActualNumberOfCellInfoEntries; //#define SHOW_SHORT_CELL_VOLTAGES // Print 3 digits cell voltage (value - 3.0 V) on Cell Info page. Enables display of up to 20 voltages or additional information. #if defined(SHOW_SHORT_CELL_VOLTAGES) if (tNumberOfCellInfoEntries > 15) { @@ -287,15 +292,15 @@ void printCellInfoOnLCD() { */ if (i == 4) { // print SOC - sprintf_P(sStringBuffer, PSTR("%3u%%"), sJKFAllReplyPointer->SOCPercent); + sprintf_P(sStringBuffer, PSTR("%3u%%"), JK_BMS_1.JKAllReplyPointer->SOCPercent); myLCD.print(sStringBuffer); } else if (i == 8) { // Print current in the last 4 characters - printFloatValueRightAlignedOnLCD(JKComputedData.BatteryLoadCurrentFloat, 4); + printFloatValueRightAlignedOnLCD(JK_BMS_1.JKComputedData.BatteryLoadCurrentFloat, 4); } else if (i == 12) { // print " A " or " A B" myLCD.print(F(" A ")); - if (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { + if (JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { myLCD.print('B'); } else { myLCD.print(' '); @@ -307,21 +312,21 @@ void printCellInfoOnLCD() { } // print maximum or minimum indicator - if (JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MAXIMUM) { + if (JK_BMS_1.JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MAXIMUM) { myLCD.print((char) (0x1)); - } else if (JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MINIMUM) { + } else if (JK_BMS_1.JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MINIMUM) { myLCD.print((char) (0x2)); } else { myLCD.print(' '); } // print fix format 3 character value - sprintf_P(sStringBuffer, PSTR("%3d"), JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt - 3000); // Value can be negative! + sprintf_P(sStringBuffer, PSTR("%3d"), JK_BMS_1.JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt - 3000); // Value can be negative! myLCD.print(sStringBuffer); } if (tNumberOfCellInfoEntries > 0 && tNumberOfCellInfoEntries <= 16) { // print voltage difference myLCD.setCursor(17, 3); - printFloatValueRightAlignedOnLCD(JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt / 100.0, 3, true); // true = no leading space + printFloatValueRightAlignedOnLCD(JK_BMS_1.JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt / 100.0, 3, true); // true = no leading space } #else if (tNumberOfCellInfoEntries > 12) { @@ -341,15 +346,15 @@ void printCellInfoOnLCD() { } // print maximum or minimum indicator - if (JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MAXIMUM) { + if (JK_BMS_1.JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MAXIMUM) { myLCD.print((char) (0x1)); - } else if (JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MINIMUM) { + } else if (JK_BMS_1.JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween == VOLTAGE_IS_MINIMUM) { myLCD.print((char) (0x2)); } else { myLCD.print(' '); } - myLCD.print(JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt); + myLCD.print(JK_BMS_1.JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt); } #endif } @@ -364,7 +369,7 @@ void printCellStatisticsOnLCD() { bool tDisplayCellMinimumStatistics = sToggleDisplayCounter & CELL_STATISTICS_COUNTER_MASK; // 0x04 sToggleDisplayCounter++; - auto tNumberOfCellInfoEntries = JKConvertedCellInfo.ActualNumberOfCellInfoEntries; + auto tNumberOfCellInfoEntries = JK_BMS_1.JKConvertedCellInfo.ActualNumberOfCellInfoEntries; uint_fast8_t tRowNumber; if (tNumberOfCellInfoEntries > 12) { tRowNumber = 0; @@ -384,9 +389,9 @@ void printCellStatisticsOnLCD() { for (uint8_t i = 0; i < tNumberOfCellInfoEntries; ++i) { uint8_t tPercent; if (tDisplayCellMinimumStatistics) { - tPercent = CellStatistics.CellMinimumPercentageArray[i]; + tPercent = JK_BMS_1.CellStatistics.CellMinimumPercentageArray[i]; } else { - tPercent = CellStatistics.CellMaximumPercentageArray[i]; + tPercent = JK_BMS_1.CellStatistics.CellMaximumPercentageArray[i]; } if (tPercent < 10) { myLCD.print(' '); @@ -446,11 +451,11 @@ void printCapacityInfoOnLCD() { void printBigInfoOnLCD() { bigNumberLCD.setBigNumberCursor(0, 0); - bigNumberLCD.print(sJKFAllReplyPointer->SOCPercent); + bigNumberLCD.print(JK_BMS_1.JKAllReplyPointer->SOCPercent); uint8_t tColumn; - if (sJKFAllReplyPointer->SOCPercent < 10) { + if (JK_BMS_1.JKAllReplyPointer->SOCPercent < 10) { tColumn = 3; - } else if (sJKFAllReplyPointer->SOCPercent < 100) { + } else if (JK_BMS_1.JKAllReplyPointer->SOCPercent < 100) { tColumn = 6; } else { tColumn = 8; // 100% @@ -463,7 +468,7 @@ void printBigInfoOnLCD() { */ uint8_t tAvailableColumns = (LCD_COLUMNS - 2) - tColumn; // 14, 11, 9. -2 for the trailing W or KW char tKiloWattChar = ' '; - int16_t tBatteryLoadPower = JKComputedData.BatteryLoadPower; + int16_t tBatteryLoadPower = JK_BMS_1.JKComputedData.BatteryLoadPower; /* * First print string to buffer */ @@ -472,7 +477,7 @@ void printBigInfoOnLCD() { float tBatteryLoadPowerFloat = tBatteryLoadPower * 0.001; // convert to kW dtostrf(tBatteryLoadPowerFloat, 5, 2, sStringBuffer); } else { - sprintf_P(sStringBuffer, PSTR("%d"), JKComputedData.BatteryLoadPower); + sprintf_P(sStringBuffer, PSTR("%d"), JK_BMS_1.JKComputedData.BatteryLoadPower); } /* @@ -528,14 +533,14 @@ void printBigInfoOnLCD() { * Bottom row: Max temperature, current, voltage difference and the actual states */ myLCD.setCursor(0, 3); - myLCD.print(JKComputedData.TemperatureMaximum); + myLCD.print(JK_BMS_1.JKComputedData.TemperatureMaximum); myLCD.print(F(DEGREE_SIGN_STRING " ")); myLCD.setCursor(4, 3); - printFloatValueRightAlignedOnLCD(JKComputedData.BatteryLoadCurrentFloat, 5); + printFloatValueRightAlignedOnLCD(JK_BMS_1.JKComputedData.BatteryLoadCurrentFloat, 5); myLCD.print('A'); - printFloatValueRightAlignedOnLCD(JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt / 100.0, 5); + printFloatValueRightAlignedOnLCD(JK_BMS_1.JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt / 100.0, 5); myLCD.print('V'); myLCD.setCursor(17, 3); // Last 3 characters are the actual states @@ -546,10 +551,10 @@ void printCANInfoOnLCD() { /* * sLCDDisplayPageNumber == JK_BMS_PAGE_CAN_INFO */ - if (!sCANDataIsInitialized || JKComputedData.BMSIsStarting) { + if (!sCANDataIsInitialized || JK_BMS_1.JKComputedData.BMSIsStarting) { myLCD.print(F("No CAN data are sent")); myLCD.setCursor(0, 1); - if (JKComputedData.BMSIsStarting) { + if (JK_BMS_1.JKComputedData.BMSIsStarting) { myLCD.print(F("BMS is starting")); } else { myLCD.print(F("No BMS data received")); @@ -639,16 +644,16 @@ void printCANInfoOnLCD() { void printVoltageCurrentAndPowerOnLCD() { myLCD.setCursor(0, 2); // Voltage -// myLCD.print(JKComputedData.BatteryVoltageFloat, 2); // currently requires more programming space - printFloatValueRightAlignedOnLCD(JKComputedData.BatteryVoltageFloat, 5, true); // true -> do not print leading space +// myLCD.print(JK_BMS_1.JKComputedData.BatteryVoltageFloat, 2); // currently requires more programming space + printFloatValueRightAlignedOnLCD(JK_BMS_1.JKComputedData.BatteryVoltageFloat, 5, true); // true -> do not print leading space myLCD.print(F("V ")); // Current - printFloatValueRightAlignedOnLCD(JKComputedData.BatteryLoadCurrentFloat, 5); + printFloatValueRightAlignedOnLCD(JK_BMS_1.JKComputedData.BatteryLoadCurrentFloat, 5); myLCD.print('A'); // Power - sprintf_P(sStringBuffer, PSTR("%6d"), JKComputedData.BatteryLoadPower); // force use of 6 columns + sprintf_P(sStringBuffer, PSTR("%6d"), JK_BMS_1.JKComputedData.BatteryLoadPower); // force use of 6 columns myLCD.print(sStringBuffer); myLCD.print('W'); } @@ -664,24 +669,24 @@ void printVoltageDifferenceAndTemperature() { * show all 3 temperatures instead of voltage difference and 2 temperatures */ bool tShowBothExternalTemperatures = true; - if (JKComputedData.TemperatureSensor1 > 0 && JKComputedData.TemperatureSensor2 > 0 - && abs(JKComputedData.TemperatureSensor1 - JKComputedData.TemperatureSensor2) - < (min(JKComputedData.TemperatureSensor1, JKComputedData.TemperatureSensor1) / 4)) { + if (JK_BMS_1.JKComputedData.TemperatureSensor1 > 0 && JK_BMS_1.JKComputedData.TemperatureSensor2 > 0 + && abs(JK_BMS_1.JKComputedData.TemperatureSensor1 - JK_BMS_1.JKComputedData.TemperatureSensor2) + < (min(JK_BMS_1.JKComputedData.TemperatureSensor1, JK_BMS_1.JKComputedData.TemperatureSensor1) / 4)) { tShowBothExternalTemperatures = false; - printFloatValueRightAlignedOnLCD(JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt / 100.0, 5, true); // true = no leading space + printFloatValueRightAlignedOnLCD(JK_BMS_1.JKComputedData.BatteryVoltageDifferenceToEmpty10Millivolt / 100.0, 5, true); // true = no leading space myLCD.print(F("V ")); } - myLCD.print(JKComputedData.TemperaturePowerMosFet); + myLCD.print(JK_BMS_1.JKComputedData.TemperaturePowerMosFet); myLCD.print(F(DEGREE_SIGN_STRING "C ")); if (tShowBothExternalTemperatures) { // show both external sensors - myLCD.print(JKComputedData.TemperatureSensor1); + myLCD.print(JK_BMS_1.JKComputedData.TemperatureSensor1); myLCD.print(F(DEGREE_SIGN_STRING "C ")); - myLCD.print(JKComputedData.TemperatureSensor2); + myLCD.print(JK_BMS_1.JKComputedData.TemperatureSensor2); myLCD.print(F(DEGREE_SIGN_STRING "C ")); } else { // show maximum of the 2 external sensors - myLCD.print(max(JKComputedData.TemperatureSensor1, JKComputedData.TemperatureSensor2)); + myLCD.print(max(JK_BMS_1.JKComputedData.TemperatureSensor1, JK_BMS_1.JKComputedData.TemperatureSensor2)); myLCD.print(F(DEGREE_SIGN_STRING "C ")); } } @@ -706,9 +711,10 @@ void printTimeoutMessageOnLCD() { */ void printAlarmInfoOnLCD() { myLCD.clear(); + uint8_t tAlarmIndexToShowOnLCD = JK_BMS_1.AlarmIndexToShowOnLCD; // Copy alarm message from flash, but not more than 20 characters - const char *tLastAlarmString = (char*) (pgm_read_word(&JK_BMSAlarmStringsArray[sAlarmIndexToShowOnLCD])); + const char *tLastAlarmString = (char*) (pgm_read_word(&JK_BMSAlarmStringsArray[tAlarmIndexToShowOnLCD])); strncpy_P(sStringBuffer, tLastAlarmString, LCD_COLUMNS); sStringBuffer[LCD_COLUMNS] = '\0'; myLCD.print(sStringBuffer); @@ -719,18 +725,18 @@ void printAlarmInfoOnLCD() { * or remainder of alarm string and uptime */ myLCD.setCursor(0, 1); - if (sAlarmIndexToShowOnLCD == INDEX_OF_DISCHARGING_UNDERVOLTAGE_ALARM - || sAlarmIndexToShowOnLCD == INDEX_OF_CHARGING_OVERVOLTAGE_ALARM) { + if (tAlarmIndexToShowOnLCD == INDEX_OF_DISCHARGING_UNDERVOLTAGE_ALARM + || tAlarmIndexToShowOnLCD == INDEX_OF_CHARGING_OVERVOLTAGE_ALARM) { uint16_t tCellMillivoltToPrint; uint8_t tCellIndexToPrint; - if (sAlarmIndexToShowOnLCD == INDEX_OF_DISCHARGING_UNDERVOLTAGE_ALARM) { + if (tAlarmIndexToShowOnLCD == INDEX_OF_DISCHARGING_UNDERVOLTAGE_ALARM) { // Index of minimum cell - tCellIndexToPrint = JKConvertedCellInfo.IndexOfMinimumCellMillivolt; - tCellMillivoltToPrint = JKConvertedCellInfo.MinimumCellMillivolt; - } else if (sAlarmIndexToShowOnLCD == INDEX_OF_CHARGING_OVERVOLTAGE_ALARM) { + tCellIndexToPrint = JK_BMS_1.JKConvertedCellInfo.IndexOfMinimumCellMillivolt; + tCellMillivoltToPrint = JK_BMS_1.JKConvertedCellInfo.MinimumCellMillivolt; + } else if (tAlarmIndexToShowOnLCD == INDEX_OF_CHARGING_OVERVOLTAGE_ALARM) { // Index of maximum cell - tCellIndexToPrint = JKConvertedCellInfo.IndexOfMaximumCellMillivolt; - tCellMillivoltToPrint = JKConvertedCellInfo.MaximumCellMillivolt; + tCellIndexToPrint = JK_BMS_1.JKConvertedCellInfo.IndexOfMaximumCellMillivolt; + tCellMillivoltToPrint = JK_BMS_1.JKConvertedCellInfo.MaximumCellMillivolt; } // print millivolt with fix format 3 character value sprintf_P(sStringBuffer, PSTR("%2d %3dmV "), tCellIndexToPrint + 1, tCellMillivoltToPrint - 3000); @@ -760,7 +766,7 @@ void printAlarmInfoOnLCD() { } /* - * If sAlarmIndexToShowOnLCD != INDEX_NO_ALARM show only actual HEX alarm bits in row 4 + * If AlarmIndexToShowOnLCD != INDEX_NO_ALARM show only actual HEX alarm bits in row 4 */ void printOverwiewOrAlarmInfoOnLCD() { if (!sShowAlarmInsteadOfOverview) { @@ -775,10 +781,10 @@ void printOverwiewOrAlarmInfoOnLCD() { */ myLCD.setCursor(0, 1); // Percent of charge - myLCD.print(sJKFAllReplyPointer->SOCPercent); + myLCD.print(JK_BMS_1.JKAllReplyPointer->SOCPercent); myLCD.print(F("% ")); // Remaining capacity - sprintf_P(sStringBuffer, PSTR("%3d"), JKComputedData.RemainingCapacityAmpereHour); + sprintf_P(sStringBuffer, PSTR("%3d"), JK_BMS_1.JKComputedData.RemainingCapacityAmpereHour); myLCD.print(sStringBuffer); myLCD.print(F("Ah ")); // Last 3 characters are the enable states @@ -810,18 +816,28 @@ void printBMSDataOnLCD() { # endif ) { /* - * Check for alarm info + * Check for alarm info, which is not yet reset by main loop at time of calling */ - if (sPrintAlarmInfoOnlyOnce) { - sPrintAlarmInfoOnlyOnce = false; - sShowAlarmInsteadOfOverview = true; - sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; // Set current page - printAlarmInfoOnLCD(); // print info only once + if (JK_BMS_1.AlarmJustGetsActive) { +# if !defined(ENABLE_OVER_AND_UNDER_VOLTAGE_WARNING_ON_LCD) + if (JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmsAsWord + & ~MASK_OF_CHARGING_AND_DISCHARGING_OVERVOLTAGE_ALARM_UNSWAPPED) { + // Other than over / undervoltage alarm bit is active +# endif + /* + * fill main part of LCD alarm page only once + */ + sShowAlarmInsteadOfOverview = true; + sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; // Set current page + printAlarmInfoOnLCD(); // print info only once # if !defined(DISPLAY_ALWAYS_ON) if (checkAndTurnLCDOn()) { JK_INFO_PRINTLN(F("alarm status changing")); // Reason for switching on LCD display } # endif +# if !defined(ENABLE_OVER_AND_UNDER_VOLTAGE_WARNING_ON_LCD) + } +# endif } // do not clear alarm info, which is only printed once @@ -868,16 +884,15 @@ void checkButtonPressForLCD() { */ sPageButtonJustPressed = false; /* - * If alarm is active, only reset it and do no other action, except switching on the display if off + * If alarm is active, only reset it and do no other action, except switching the display on, if display is off */ - if (sAlarmJustGetsActive || sTimeoutJustdetected) { - sAlarmJustGetsActive = false; // disable further alarm beeps - sTimeoutJustdetected = false; // disable further timeout beeps + if (sAlarmOrTimeoutBeepActive) { + sAlarmOrTimeoutBeepActive = false; // disable further alarm beeps # if defined(DISPLAY_ALWAYS_ON) - return; // no further action, just reset flag / beep + return; // No further action, just reset flag / beep # else if (!sSerialLCDIsSwitchedOff) { - return; // no further action, just reset flag / beep + return; // No switching on LCD, just reset flag / beep } # endif } @@ -916,7 +931,7 @@ void checkButtonPressForLCD() { */ if (tLCDDisplayPageNumber == JK_BMS_PAGE_CAN_INFO) { // Button is still pressed on CAN Info page -> enable serial debug output as long as button is pressed - sDebugModeActivated = true; // Is set to false in loop + sCommunicationDebugModeActivated = true; // Is set to false in loop #if !defined(NO_ANALYTICS) } else if (tLCDDisplayPageNumber == JK_BMS_PAGE_CAPACITY_INFO) { @@ -957,7 +972,7 @@ void checkButtonPressForLCD() { /* * Not page JK_BMS_PAGE_CAN_INFO or JK_BMS_PAGE_CAPACITY_INFO -> switch to CAN Info page */ - sDebugModeActivated = true; // Is set to false in loop + sCommunicationDebugModeActivated = true; // Is set to false in loop if (sSerialLCDAvailable) { JK_INFO_PRINTLN(); JK_INFO_PRINTLN(F("Long press detected -> switch to CAN page and activate one time debug print")); @@ -1018,7 +1033,7 @@ void setLCDDisplayPage(uint8_t aLCDDisplayPageNumber, bool aDoNotPrint) { #if !defined(NO_ANALYTICS) if (aLCDDisplayPageNumber == JK_BMS_PAGE_CAPACITY_INFO) { // do it even if we have timeout - sDebugModeActivated = false; // Disable every debug output after entering this page + sCommunicationDebugModeActivated = false; // Disable every debug output after entering this page printCapacityInfoOnLCD(); // First update LCD before printing the plotter data readAndPrintSOCData(); // this takes a while... } @@ -1029,7 +1044,7 @@ void setLCDDisplayPage(uint8_t aLCDDisplayPageNumber, bool aDoNotPrint) { void testLCDPages() { sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; printBMSDataOnLCD(); - delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); + delay (LCD_MESSAGE_PERSIST_TIME_MILLIS); sLCDDisplayPageNumber = JK_BMS_PAGE_CELL_INFO; // Create symbols character for maximum and minimum @@ -1042,48 +1057,49 @@ void testLCDPages() { printBMSDataOnLCD(); delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); - Serial.println(F("Test alarms")); + Serial.println(F("testLCDPages: Test alarms")); /* * Test alarms */ - sJKFAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm = true; - detectAndPrintAlarmInfo(); // this sets the LCD alarm string + JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm = true; + JK_BMS_1.detectAndPrintAlarmInfo(); // this sets the LCD alarm string printBMSDataOnLCD(); - sJKFAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm = false; + JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm = false; delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); - sJKFAllReplyPointer->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm = true; - detectAndPrintAlarmInfo(); // this sets the LCD alarm string + JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm = true; + JK_BMS_1.detectAndPrintAlarmInfo(); // this sets the LCD alarm string printBMSDataOnLCD(); - sJKFAllReplyPointer->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm = false; + JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm = false; delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); /* * PowerMosFetOvertemperatureAlarm */ - sJKFAllReplyPointer->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm = true; - JKComputedData.TemperaturePowerMosFet = 90; - JKComputedData.TemperatureSensor1 = 25; - detectAndPrintAlarmInfo(); // this sets the LCD alarm string + JK_BMS_1.JKAllReplyPointer->AlarmUnion.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(); - sJKFAllReplyPointer->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm = false; - JKComputedData.TemperaturePowerMosFet = 33; + JK_BMS_1.JKAllReplyPointer->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm = false; + JK_BMS_1.JKComputedData.TemperaturePowerMosFet = 33; delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); // reset alarm - sAlarmIndexToShowOnLCD = INDEX_NO_ALARM; - sAlarmJustGetsActive = false; + JK_BMS_1.AlarmIndexToShowOnLCD = INDEX_NO_ALARM; + JK_BMS_1.AlarmJustGetsActive = false; sShowAlarmInsteadOfOverview = false; - Serial.println(F("Test maximum values")); + Serial.println(F("testLCDPages: Test maximum values")); /* * Check display of maximum values */ - sJKFAllReplyPointer->SOCPercent = 100; - JKComputedData.BatteryLoadPower = -11000; - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; - JKComputedData.TemperaturePowerMosFet = 111; - JKComputedData.TemperatureSensor1 = 100; + JK_BMS_1.JKAllReplyPointer->SOCPercent = 100; + JK_BMS_1.JKComputedData.BatteryLoadPower = -11000; + JK_BMS_1.JKComputedData.BatteryLoadCurrentFloat = JK_BMS_1.JKComputedData.BatteryLoadPower + / JK_BMS_1.JKComputedData.BatteryVoltageFloat; + JK_BMS_1.JKComputedData.TemperaturePowerMosFet = 111; + JK_BMS_1.JKComputedData.TemperatureSensor1 = 100; sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; printBMSDataOnLCD(); delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); @@ -1097,12 +1113,14 @@ void testLCDPages() { printBMSDataOnLCD(); delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); + Serial.println(F("testLCDPages: Test other values")); /* * Test other values */ - sJKFAllReplyPointer->SOCPercent = 1; - JKComputedData.BatteryLoadPower = 12345; - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; + JK_BMS_1.JKAllReplyPointer->SOCPercent = 1; + JK_BMS_1.JKComputedData.BatteryLoadPower = 12345; + JK_BMS_1.JKComputedData.BatteryLoadCurrentFloat = JK_BMS_1.JKComputedData.BatteryLoadPower + / JK_BMS_1.JKComputedData.BatteryVoltageFloat; sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; printBMSDataOnLCD(); @@ -1113,10 +1131,11 @@ void testLCDPages() { printBMSDataOnLCD(); delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); - sJKFAllReplyPointer->SOCPercent = 100; - JKComputedData.BatteryLoadCurrentFloat = -100; + JK_BMS_1.JKAllReplyPointer->SOCPercent = 100; + JK_BMS_1.JKComputedData.BatteryLoadCurrentFloat = -100; sLCDDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; printBMSDataOnLCD(); + Serial.println(F("testLCDPages: End")); delay(LCD_MESSAGE_PERSIST_TIME_MILLIS); } @@ -1131,32 +1150,38 @@ void testBigNumbers() { /* * test with positive numbers */ - JKComputedData.BatteryLoadPower = 12345; - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; + JK_BMS_1.JKComputedData.BatteryLoadPower = 12345; + JK_BMS_1.JKComputedData.BatteryLoadCurrentFloat = JK_BMS_1.JKComputedData.BatteryLoadPower + / JK_BMS_1.JKComputedData.BatteryVoltageFloat; for (int i = 0; i < 5; ++i) { delay(2 * LCD_MESSAGE_PERSIST_TIME_MILLIS); myLCD.clear(); printBMSDataOnLCD(); - JKComputedData.BatteryLoadPower /= 10; // 1234 -> 12 - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; + JK_BMS_1.JKComputedData.BatteryLoadPower /= 10; // 1234 -> 12 + JK_BMS_1.JKComputedData.BatteryLoadCurrentFloat = JK_BMS_1.JKComputedData.BatteryLoadPower + / JK_BMS_1.JKComputedData.BatteryVoltageFloat; } /* * test with negative numbers */ - JKComputedData.BatteryLoadPower = -12345; - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; + JK_BMS_1.JKComputedData.BatteryLoadPower = -12345; + JK_BMS_1.JKComputedData.BatteryLoadCurrentFloat = JK_BMS_1.JKComputedData.BatteryLoadPower + / JK_BMS_1.JKComputedData.BatteryVoltageFloat; for (int i = 0; i < 5; ++i) { delay(2 * LCD_MESSAGE_PERSIST_TIME_MILLIS); myLCD.clear(); printBMSDataOnLCD(); - JKComputedData.BatteryLoadPower /= 10; // 1234 -> 12 - JKComputedData.BatteryLoadCurrentFloat = JKComputedData.BatteryLoadPower / JKComputedData.BatteryVoltageFloat; + JK_BMS_1.JKComputedData.BatteryLoadPower /= 10; // 1234 -> 12 + JK_BMS_1.JKComputedData.BatteryLoadCurrentFloat = JK_BMS_1.JKComputedData.BatteryLoadPower + / JK_BMS_1.JKComputedData.BatteryVoltageFloat; } - sJKFAllReplyPointer->SOCPercent /= 10; + JK_BMS_1.JKAllReplyPointer->SOCPercent /= 10; } + Serial.println(F("Test BigNumbers: end")); + } #endif // STANDALONE_TEST diff --git a/JK-BMSToPylontechCAN/LCDPrintUtils.hpp b/JK-BMSToPylontechCAN/LCDPrintUtils.hpp index 9ee0e62..47e7812 100644 --- a/JK-BMSToPylontechCAN/LCDPrintUtils.hpp +++ b/JK-BMSToPylontechCAN/LCDPrintUtils.hpp @@ -153,6 +153,8 @@ void printFloatValueRightAlignedOnLCD(float aFloatValue, uint8_t aNumberOfCharac } void testPrintFloatValueRightAlignedOnLCD() { + Serial.println(F("testPrintFloatValueRightAlignedOnLCD: 1.")); + myLCD.clear(); float tTestValue = 123.45; printFloatValueRightAlignedOnLCD(tTestValue, 6); @@ -187,6 +189,7 @@ void testPrintFloatValueRightAlignedOnLCD() { // Result="-.1234-.123-.12-.1-0" delay(4000); + Serial.println(F("testPrintFloatValueRightAlignedOnLCD: 2.")); myLCD.clear(); tTestValue = 123.45; @@ -223,6 +226,8 @@ void testPrintFloatValueRightAlignedOnLCD() { // Result=".12344.1234.123.12.1" delay(4000); + Serial.println(F("testPrintFloatValueRightAlignedOnLCD: End")); + } #if defined(LOCAL_DEBUG) diff --git a/JK-BMSToPylontechCAN/Pylontech_CAN.h b/JK-BMSToPylontechCAN/Pylontech_CAN.h index 80c6219..0e8f9a0 100644 --- a/JK-BMSToPylontechCAN/Pylontech_CAN.h +++ b/JK-BMSToPylontechCAN/Pylontech_CAN.h @@ -27,7 +27,6 @@ // Based on information in: // https://www.setfirelabs.com/green-energy/pylontech-can-reading-can-replication - #ifndef _PYLONTECH_CAN_H #define _PYLONTECH_CAN_H @@ -74,7 +73,7 @@ extern struct PylontechCANSMACapacityFrame35FStruct PylontechCANSMACapacityFrame extern struct BYDCANCellLimitsFrame373Struct BYDCANCellLimitsFrame373; extern struct PylontechCANLuxpowerCapacityFrame379Struct PylontechCANLuxpowerCapacityFrame379; -void fillAllCANData(struct JKReplyStruct *aJKFAllReply); +void fillAllCANData(JK_BMS *aJK_BMS_Ptr); void sendAllCANFrames(bool aDebugModeActive); void modifyAllCanDataToInactive(); @@ -103,16 +102,18 @@ struct PylontechCANAliveFrame305Struct { struct PylontechCANBatteryLimitsFrame351Struct { struct CANFrameInfoStruct CANFrameInfo = { PYLON_CAN_BATTERY_LIMITS_FRAME_ID, 8 }; // 0x351 struct { - int16_t BatteryChargeOvervoltage100Millivolt; // 0 to 750 | Maximum of all, so you can disable one by setting its value low + int16_t BatteryChargeOvervoltage100Millivolt; // 0 to 750 | Maximum of all, so you can disable one by setting its value low int16_t BatteryChargeCurrentLimit100Milliampere; // 0 to 5000 | SUM of all Charge enabled modules int16_t BatteryDischargeCurrentLimit100Milliampere; // -5000 to 0 | SUM of all Discharge enabled modules - int16_t BatteryDischarge100Millivolt; // 0 to 65535 | Minimum of all, so you can disable one by setting its value high + int16_t BatteryDischarge100Millivolt; // 0 to 65535 | Minimum of all, so you can disable one by setting its value high } FrameData; - void fillFrame(struct JKReplyStruct *aJKFAllReply) { - FrameData.BatteryChargeOvervoltage100Millivolt = JKComputedData.BatteryFullVoltage10Millivolt / 10; - FrameData.BatteryChargeCurrentLimit100Milliampere = swap(aJKFAllReply->ChargeOvercurrentProtectionAmpere) * 10; - FrameData.BatteryDischargeCurrentLimit100Milliampere = swap(aJKFAllReply->DischargeOvercurrentProtectionAmpere) * 10; - FrameData.BatteryDischarge100Millivolt = JKComputedData.BatteryEmptyVoltage10Millivolt / 10; + 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; } }; @@ -129,8 +130,8 @@ struct PylontechCANSohSocFrame355Struct { uint16_t SOCHighDefinition100PPM = 10000; // for SMA Sunny Island inverters #endif } FrameData; - void fillFrame(struct JKReplyStruct *aJKFAllReply) { - FrameData.SOCPercent = aJKFAllReply->SOCPercent; + void fillFrame(JK_BMS *aJK_BMS_Ptr) { + FrameData.SOCPercent = aJK_BMS_Ptr->JKAllReplyPointer->SOCPercent; } }; @@ -144,11 +145,10 @@ struct PylontechCANCurrentValuesFrame356Struct { int16_t Current100Milliampere; // -2500 to 2500 | Sum of all int16_t Temperature100Millicelsius; // -500 to 750 | Maximum of all } FrameData; - void fillFrame(struct JKReplyStruct *aJKFAllReply) { - (void) aJKFAllReply; // To avoid [-Wunused-parameter] warning - FrameData.Voltage10Millivolt = JKComputedData.BatteryVoltage10Millivolt; - FrameData.Current100Milliampere = JKComputedData.Battery10MilliAmpere / 10; - FrameData.Temperature100Millicelsius = JKComputedData.TemperatureMaximum * 10; + void fillFrame(JK_BMS *aJK_BMS_Ptr) { + FrameData.Voltage10Millivolt = aJK_BMS_Ptr->JKComputedData.BatteryVoltage10Millivolt; + FrameData.Current100Milliampere = aJK_BMS_Ptr->JKComputedData.Battery10MilliAmpere / 10; + FrameData.Temperature100Millicelsius = aJK_BMS_Ptr->JKComputedData.TemperatureMaximum * 10; } }; @@ -203,42 +203,43 @@ struct PylontechCANErrorsWarningsFrame359Struct { uint8_t Token1 = 0x50; // 'P' uint8_t Token2 = 0x4E; // 'N' } FrameData; - void fillFrame(struct JKReplyStruct *aJKFAllReply) { + void fillFrame(JK_BMS *aJK_BMS_Ptr) { + struct JKReplyStruct *tJKFAllReply = aJK_BMS_Ptr->JKAllReplyPointer; /* * 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 = aJKFAllReply->AlarmUnion.AlarmBits.CellOvervoltageAlarm; - FrameData.CellUndervoltageError = aJKFAllReply->AlarmUnion.AlarmBits.CellUndervoltageAlarm; - FrameData.CellOvertemperatureError = aJKFAllReply->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm - || aJKFAllReply->AlarmUnion.AlarmBits.Sensor1Or2OvertemperatureAlarm; - FrameData.CellUndertemperatureError = aJKFAllReply->AlarmUnion.AlarmBits.Sensor1Or2UndertemperatureAlarm; - FrameData.DischargeOvercurrentError = aJKFAllReply->AlarmUnion.AlarmBits.DischargeOvercurrentAlarm; + 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; // Byte 1 - FrameData.ChargeOvercurrentError = aJKFAllReply->AlarmUnion.AlarmBits.ChargeOvercurrentAlarm; - FrameData.SystemError = aJKFAllReply->BMSStatus.StatusBits.BatteryDown; -// if (aJKFAllReply->SOCPercent < 5) { + FrameData.ChargeOvercurrentError = tJKFAllReply->AlarmUnion.AlarmBits.ChargeOvercurrentAlarm; + FrameData.SystemError = tJKFAllReply->BMSStatus.StatusBits.BatteryDown; +// if (tJKFAllReply->SOCPercent < 5) { // FrameData.SystemError = 1; // } // Byte 2 // (mis)use the battery alarms as cell warnings for Pylon - FrameData.CellHighVoltageWarning = aJKFAllReply->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm; - FrameData.CellLowVoltageWarning = aJKFAllReply->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm; + FrameData.CellHighVoltageWarning = tJKFAllReply->AlarmUnion.AlarmBits.ChargeOvervoltageAlarm; + FrameData.CellLowVoltageWarning = tJKFAllReply->AlarmUnion.AlarmBits.DischargeUndervoltageAlarm; // Use the same values as for error here - FrameData.CellHighTemperatureWarning = aJKFAllReply->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm - || aJKFAllReply->AlarmUnion.AlarmBits.Sensor1Or2OvertemperatureAlarm; - FrameData.CellLowTemperatureWarning = aJKFAllReply->AlarmUnion.AlarmBits.Sensor1Or2UndertemperatureAlarm; - FrameData.DischargeHighCurrentWarning = aJKFAllReply->AlarmUnion.AlarmBits.DischargeOvercurrentAlarm; + FrameData.CellHighTemperatureWarning = tJKFAllReply->AlarmUnion.AlarmBits.PowerMosFetOvertemperatureAlarm + || tJKFAllReply->AlarmUnion.AlarmBits.Sensor1Or2OvertemperatureAlarm; + FrameData.CellLowTemperatureWarning = tJKFAllReply->AlarmUnion.AlarmBits.Sensor1Or2UndertemperatureAlarm; + FrameData.DischargeHighCurrentWarning = tJKFAllReply->AlarmUnion.AlarmBits.DischargeOvercurrentAlarm; // Byte 3 // Use the same values as for error here - FrameData.ChargeHighCurrentWarning = aJKFAllReply->AlarmUnion.AlarmBits.ChargeOvercurrentAlarm; - FrameData.SystemError = aJKFAllReply->BMSStatus.StatusBits.BatteryDown; + FrameData.ChargeHighCurrentWarning = tJKFAllReply->AlarmUnion.AlarmBits.ChargeOvercurrentAlarm; + FrameData.SystemError = tJKFAllReply->BMSStatus.StatusBits.BatteryDown; -// if (aJKFAllReply->SOCPercent < 10) { +// if (tJKFAllReply->SOCPercent < 10) { // FrameData.SystemWarning = 1; // } @@ -271,11 +272,12 @@ struct PylontechCANBatteryRequesFrame35CStruct { bool ChargeEnable :1; uint8_t Filler = 0; } FrameData; - void fillFrame(struct JKReplyStruct *aJKFAllReply) { + void fillFrame(JK_BMS *aJK_BMS_Ptr) { FrameData.FullChargeRequest = 0; + struct JKReplyStruct *tJKFAllReply = aJK_BMS_Ptr->JKAllReplyPointer; #if SOC_THRESHOLD_FOR_FORCE_CHARGE_REQUEST_I > 0 - if (aJKFAllReply->SOCPercent < SOC_THRESHOLD_FOR_FORCE_CHARGE_REQUEST_I) { + if (tJKFAllReply->SOCPercent < SOC_THRESHOLD_FOR_FORCE_CHARGE_REQUEST_I) { // ForceChargeRequestI forces the inverter to charge the battery from any available power source regardless of inverter settings FrameData.ForceChargeRequestI = 1; } else { @@ -285,13 +287,13 @@ struct PylontechCANBatteryRequesFrame35CStruct { FrameData.ForceChargeRequestI = 0; #endif // If battery drops below lower voltage. See https://powerforum.co.za/topic/13587-battery-anomaly-on-synsynk-hybrid-inverter/ - if (swap(aJKFAllReply->Battery10Millivolt) < JKComputedData.BatteryEmptyVoltage10Millivolt) { + if (swap(tJKFAllReply->Battery10Millivolt) < aJK_BMS_Ptr->JKComputedData.BatteryEmptyVoltage10Millivolt) { FrameData.ForceChargeRequestII = 1; } else { FrameData.ForceChargeRequestII = 0; } - FrameData.DischargeEnable = aJKFAllReply->BMSStatus.StatusBits.ChargeMosFetActive; - FrameData.ChargeEnable = aJKFAllReply->BMSStatus.StatusBits.DischargeMosFetActive; + FrameData.DischargeEnable = tJKFAllReply->BMSStatus.StatusBits.ChargeMosFetActive; + FrameData.ChargeEnable = tJKFAllReply->BMSStatus.StatusBits.DischargeMosFetActive; } }; @@ -324,10 +326,11 @@ struct PylontechCANSMACapacityFrame35FStruct { uint8_t SoftwareVersionLowByte; uint8_t SoftwareVersionHighByte; } FrameData; - void fillFrame(struct JKReplyStruct *aJKFAllReply) { - FrameData.SoftwareVersionLowByte = aJKFAllReply->SoftwareVersionNumber[1]; - FrameData.SoftwareVersionHighByte = aJKFAllReply->SoftwareVersionNumber[0]; - FrameData.CapacityAmpereHour = JKComputedData.TotalCapacityAmpereHour; + void fillFrame(JK_BMS *aJK_BMS_Ptr) { + struct JKReplyStruct *tJKFAllReply = aJK_BMS_Ptr->JKAllReplyPointer; + FrameData.SoftwareVersionLowByte = tJKFAllReply->SoftwareVersionNumber[1]; + FrameData.SoftwareVersionHighByte = tJKFAllReply->SoftwareVersionNumber[0]; + FrameData.CapacityAmpereHour = aJK_BMS_Ptr->JKComputedData.TotalCapacityAmpereHour; } }; @@ -344,15 +347,16 @@ struct BYDCANCellLimitsFrame373Struct { uint16_t CellTemperatureMinimumKelvin; // uint16_t CellTemperatureMaximumKelvin; // } FrameData; - void fillFrame(struct JKReplyStruct *aJKFAllReply) { - FrameData.CellVoltageMinimumMilliVolt = swap(aJKFAllReply->CellUndervoltageProtectionMillivolt); - FrameData.CellVoltageMaximumMilliVolt = swap(aJKFAllReply->CellOvervoltageProtectionMillivolt); + void fillFrame(JK_BMS *aJK_BMS_Ptr) { + struct JKReplyStruct *tJKFAllReply = aJK_BMS_Ptr->JKAllReplyPointer; + FrameData.CellVoltageMinimumMilliVolt = swap(tJKFAllReply->CellUndervoltageProtectionMillivolt); + FrameData.CellVoltageMaximumMilliVolt = swap(tJKFAllReply->CellOvervoltageProtectionMillivolt); FrameData.CellTemperatureMinimumKelvin = - min(swap(aJKFAllReply->ChargeUndertemperatureProtection), swap(aJKFAllReply->DischargeUndertemperatureProtection)) - + 273; + min(swap(tJKFAllReply->ChargeUndertemperatureProtection), swap(tJKFAllReply->DischargeUndertemperatureProtection)) + + 273; FrameData.CellTemperatureMaximumKelvin = - max(swap(aJKFAllReply->ChargeOvertemperatureProtection), swap(aJKFAllReply->DischargeOvertemperatureProtection)) - + 273; + max(swap(tJKFAllReply->ChargeOvertemperatureProtection), swap(tJKFAllReply->DischargeOvertemperatureProtection)) + + 273; } }; @@ -368,9 +372,8 @@ struct PylontechCANLuxpowerCapacityFrame379Struct { uint16_t Unknown1; uint32_t Unknown2; } FrameData; - void fillFrame(struct JKReplyStruct *aJKFAllReply) { - (void) aJKFAllReply; // To avoid [-Wunused-parameter] warning - FrameData.CapacityAmpereHour = JKComputedData.TotalCapacityAmpereHour; + void fillFrame(JK_BMS *aJK_BMS_Ptr) { + FrameData.CapacityAmpereHour = aJK_BMS_Ptr->JKComputedData.TotalCapacityAmpereHour; } }; #endif // _PYLONTECH_CAN_H diff --git a/JK-BMSToPylontechCAN/Pylontech_CAN.hpp b/JK-BMSToPylontechCAN/Pylontech_CAN.hpp index ea78ef7..de0fa97 100644 --- a/JK-BMSToPylontechCAN/Pylontech_CAN.hpp +++ b/JK-BMSToPylontechCAN/Pylontech_CAN.hpp @@ -51,20 +51,20 @@ struct PylontechCANAliveFrame305Struct PylontechCANAliveFrame305; void modifyCANData(); // user function, which currently enables the function to reduce max current at high SOC level -void fillAllCANData(struct JKReplyStruct *aJKFAllReply) { - PylontechCANBatteryLimitsFrame351.fillFrame(aJKFAllReply); - PylontechCANSohSocFrame355.fillFrame(aJKFAllReply); - PylontechCANBatteryRequestFrame35C.fillFrame(aJKFAllReply); - PylontechCANErrorsWarningsFrame359.fillFrame(aJKFAllReply); - PylontechCANCurrentValuesFrame356.fillFrame(aJKFAllReply); +void fillAllCANData(JK_BMS *aJK_BMS_Ptr) { + PylontechCANBatteryLimitsFrame351.fillFrame(aJK_BMS_Ptr); + PylontechCANSohSocFrame355.fillFrame(aJK_BMS_Ptr); + PylontechCANBatteryRequestFrame35C.fillFrame(aJK_BMS_Ptr); + PylontechCANErrorsWarningsFrame359.fillFrame(aJK_BMS_Ptr); + PylontechCANCurrentValuesFrame356.fillFrame(aJK_BMS_Ptr); #if defined(CAPACITY_35F_EXTENSIONS) - PylontechCANSMACapacityFrame35F.fillFrame(aJKFAllReply); + PylontechCANSMACapacityFrame35F.fillFrame(aJK_BMS_Ptr); #endif #if defined(CAPACITY_379_EXTENSIONS) - PylontechCANLuxpowerCapacityFrame379.fillFrame(aJKFAllReply); + PylontechCANLuxpowerCapacityFrame379.fillFrame(aJK_BMS_Ptr); #endif #if defined(BYD_LIMITS_373_EXTENSIONS) - BYDCANCellLimitsFrame373.fillFrame(aJKFAllReply); + BYDCANCellLimitsFrame373.fillFrame(aJK_BMS_Ptr); #endif #if defined(CAN_DATA_MODIFICATION) modifyCANData(); @@ -157,13 +157,14 @@ void sendAllCANFrames(bool aDebugModeActive) { #if !defined(USE_OWN_MODIFY_FUNCTION) && !defined(USE_CCCV_MODIFY_FUNCTION) void modifyCANData() { - if (sJKFAllReplyPointer->SOCPercent >= MAX_CURRENT_MODIFICATION_LOWER_SOC_THRESHOLD_PERCENT) { + if (JK_BMS_1.JKAllReplyPointer->SOCPercent >= MAX_CURRENT_MODIFICATION_LOWER_SOC_THRESHOLD_PERCENT) { /* * Reduce max current linear from 100% at MAX_CURRENT_MODIFICATION_LOWER_SOC_THRESHOLD (80%) SOC * to MAX_CURRENT_MODIFICATION_MIN_CURRENT_TENTHS_OF_AMPERE (1A) at 100% SOC */ - PylontechCANBatteryLimitsFrame351.FrameData.BatteryChargeCurrentLimit100Milliampere = map(sJKFAllReplyPointer->SOCPercent, - MAX_CURRENT_MODIFICATION_LOWER_SOC_THRESHOLD_PERCENT, 100, + PylontechCANBatteryLimitsFrame351.FrameData.BatteryChargeCurrentLimit100Milliampere = map( + JK_BMS_1.JKAllReplyPointer->SOCPercent, + MAX_CURRENT_MODIFICATION_LOWER_SOC_THRESHOLD_PERCENT, 100, PylontechCANBatteryLimitsFrame351.FrameData.BatteryChargeCurrentLimit100Milliampere, MAX_CURRENT_MODIFICATION_MIN_CURRENT_TENTHS_OF_AMPERE); } @@ -211,11 +212,11 @@ void modifyCANData() { return; } else { PylontechCANBatteryRequestFrame35C.FrameData.ChargeEnable = - sJKFAllReplyPointer->BMSStatus.StatusBits.DischargeMosFetActive; + JK_BMS_1.JKAllReplyPointer->BMSStatus.StatusBits.DischargeMosFetActive; } StartChargeTime = millis(); // Store starting time for charge // Get the proper charging current: either BMS limit or using 0.3C - Computed_Current_limits_100mA = min(swap(sJKFAllReplyPointer->ChargeOvercurrentProtectionAmpere) * 10, + Computed_Current_limits_100mA = min(swap(JK_BMS_1.JKAllReplyPointer->ChargeOvercurrentProtectionAmpere) * 10, JKComputedData.TotalCapacityAmpereHour * CHARGING_CURRENT_PER_CAPACITY); Serial.print(F("Charging check: >Selected Current:")); Serial.println(Computed_Current_limits_100mA); @@ -320,9 +321,9 @@ void resetCharge() { ChargeTryEffort = 0; // recover the charging limits if (PylontechCANBatteryLimitsFrame351.FrameData.BatteryChargeCurrentLimit100Milliampere - != swap(sJKFAllReplyPointer->ChargeOvercurrentProtectionAmpere) * 10) { + != swap(JK_BMS_1.JKAllReplyPointer->ChargeOvercurrentProtectionAmpere) * 10) { PylontechCANBatteryLimitsFrame351.FrameData.BatteryChargeCurrentLimit100Milliampere = swap( - sJKFAllReplyPointer->ChargeOvercurrentProtectionAmpere) * 10; + JK_BMS_1.JKAllReplyPointer->ChargeOvercurrentProtectionAmpere) * 10; } } @@ -336,15 +337,15 @@ uint8_t ReachChargeLimit() { uint16_t Charge_MilliVolt_limit = 0; if ((millis() - LastCheckTime) < CHARGE_STATUS_REFRESH_INTERVAL) return 0; // first check over voltage - if (sJKFAllReplyPointer->BatteryType == 0) { //LFP battery + if (JK_BMS_1.JKAllReplyPointer->BatteryType == 0) { //LFP battery Charge_MilliVolt_limit = 3450; - } else if (sJKFAllReplyPointer->BatteryType == 1) { //Lithium ion + } else if (JK_BMS_1.JKAllReplyPointer->BatteryType == 1) { //Lithium ion Charge_MilliVolt_limit = 4200; } // check SOC First - return (sJKFAllReplyPointer->SOCPercent >= MAX_SOC_BULK_CHARGE_THRESHOLD_PERCENT) ? 1 : 0; + return (JK_BMS_1.JKAllReplyPointer->SOCPercent >= MAX_SOC_BULK_CHARGE_THRESHOLD_PERCENT) ? 1 : 0; Serial.print(F("Battery type:")); - Serial.println(sJKFAllReplyPointer->BatteryType); + Serial.println(JK_BMS_1.JKAllReplyPointer->BatteryType); if ((JKConvertedCellInfo.MaximumCellMillivolt * 1.02) > Charge_MilliVolt_limit) { Serial.print(F("Status check::")); Serial.println((JKConvertedCellInfo.MaximumCellMillivolt * 1.02) - Charge_MilliVolt_limit); diff --git a/JK-BMSToPylontechCAN/SoftwareSerialTX.cpp b/JK-BMSToPylontechCAN/SoftwareSerialTX.cpp index a07a926..aaca6b1 100644 --- a/JK-BMSToPylontechCAN/SoftwareSerialTX.cpp +++ b/JK-BMSToPylontechCAN/SoftwareSerialTX.cpp @@ -1,29 +1,29 @@ /* -SoftwareSerialTX.h (from SoftSerial.h) - -Multi-instance software serial library for Arduino/Wiring --- Transmit-only imoplementation --- reduce footprint in code memory and RAM compared to SoftwareSerial - ~ 668 byte code - ~ 68 byte RAM + SoftwareSerialTX.h (from SoftSerial.h) - + Multi-instance software serial library for Arduino/Wiring + -- Transmit-only imoplementation + -- reduce footprint in code memory and RAM compared to SoftwareSerial + ~ 668 byte code + ~ 68 byte RAM -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. -See the GNU Lesser General Public License for more details. + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + See the GNU Lesser General Public License for more details. -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -The latest version of this library can always be found at -http://arduiniana.org. + The latest version of this library can always be found at + http://arduiniana.org. -*/ + */ #if defined(__AVR_ATmega168__) ||defined(__AVR_ATmega168P__) ||defined(__AVR_ATmega328P__) ||defined(__AVR_ATmega644P__) // @@ -36,91 +36,89 @@ The latest version of this library can always be found at // // Constructor // -SoftwareSerialTX::SoftwareSerialTX(uint8_t transmitPin) -{ - setTX(transmitPin); +SoftwareSerialTX::SoftwareSerialTX() { +} +SoftwareSerialTX::SoftwareSerialTX(uint8_t transmitPin) { + setTX(transmitPin); } -size_t SoftwareSerialTX::write(uint8_t b) -{ - // By declaring these as local variables, the compiler will put them - // in registers _before_ disabling interrupts and entering the - // critical timing sections below, which makes it a lot easier to - // verify the cycle timings - volatile uint8_t *reg = _transmitPortRegister; - uint8_t reg_mask = _transmitBitMask; - uint8_t inv_mask = ~_transmitBitMask; - uint8_t oldSREG = SREG; - uint16_t delay = _tx_delay; - - cli(); // turn off interrupts for a clean txmit - - // Write the start bit - *reg &= inv_mask; - - _delay_loop_2(delay); - - // Write each of the 8 bits - for (uint8_t i = 8; i > 0; --i) - { - if (b & 1) // choose bit - *reg |= reg_mask; // send 1 - else - *reg &= inv_mask; // send 0 +size_t SoftwareSerialTX::write(uint8_t b) { + // By declaring these as local variables, the compiler will put them + // in registers _before_ disabling interrupts and entering the + // critical timing sections below, which makes it a lot easier to + // verify the cycle timings + volatile uint8_t *reg = _transmitPortRegister; + uint8_t reg_mask = _transmitBitMask; + uint8_t inv_mask = ~_transmitBitMask; + uint8_t oldSREG = SREG; + uint16_t delay = _tx_delay; + + cli(); // turn off interrupts for a clean txmit + + // Write the start bit + *reg &= inv_mask; _delay_loop_2(delay); - b >>= 1; - } - // restore pin to natural state - *reg |= reg_mask; + // Write each of the 8 bits + for (uint8_t i = 8; i > 0; --i) { + if (b & 1) // choose bit + *reg |= reg_mask; // send 1 + else + *reg &= inv_mask; // send 0 + + _delay_loop_2(delay); + b >>= 1; + } - SREG = oldSREG; // turn interrupts back on - _delay_loop_2(delay); + // restore pin to natural state + *reg |= reg_mask; - return 1; + SREG = oldSREG; // turn interrupts back on + _delay_loop_2(delay); + + return 1; } -void SoftwareSerialTX::begin(long speed) -{ - uint16_t bit_delay = (F_CPU / speed) / 4; - //_rx_delay_centering = _rx_delay_intrabit = _rx_delay_stopbit = _tx_delay = 0; - // 12 (gcc 4.8.2) or 13 (gcc 4.3.2) cycles from start bit to first bit, - // 15 (gcc 4.8.2) or 16 (gcc 4.3.2) cycles between bits, - // 12 (gcc 4.8.2) or 14 (gcc 4.3.2) cycles from last bit to stop bit - // These are all close enough to just use 15 cycles, since the inter-bit - // timings are the most critical (deviations stack 8 times) - _tx_delay = subtract_cap(bit_delay, 15 / 4); +void SoftwareSerialTX::begin(long speed) { + uint16_t bit_delay = (F_CPU / speed) / 4; + //_rx_delay_centering = _rx_delay_intrabit = _rx_delay_stopbit = _tx_delay = 0; + // 12 (gcc 4.8.2) or 13 (gcc 4.3.2) cycles from start bit to first bit, + // 15 (gcc 4.8.2) or 16 (gcc 4.3.2) cycles between bits, + // 12 (gcc 4.8.2) or 14 (gcc 4.3.2) cycles from last bit to stop bit + // These are all close enough to just use 15 cycles, since the inter-bit + // timings are the most critical (deviations stack 8 times) + _tx_delay = subtract_cap(bit_delay, 15 / 4); } uint16_t SoftwareSerialTX::subtract_cap(uint16_t num, uint16_t sub) { - if (num > sub) - return num - sub; - else - return 1; + if (num > sub) + return num - sub; + else + return 1; } #include "digitalWriteFast.h" -void SoftwareSerialTX::setTX(uint8_t tx) -{ - // First write, then set output. If we do this the other way around, - // the pin would be output low for a short while before switching to - // output high. Now, it is input with pullup for a short while, which - // is fine. With inverse logic, either order is fine. - digitalWriteFast(tx, HIGH); - pinModeFast(tx, OUTPUT); - _transmitBitMask = digitalPinToBitMask(tx); - _transmitPortRegister = portOutputRegister(digitalPinToPort(tx)); +void SoftwareSerialTX::setTX(uint8_t tx) { + // First write, then set output. If we do this the other way around, + // the pin would be output low for a short while before switching to + // output high. Now, it is input with pullup for a short while, which + // is fine. With inverse logic, either order is fine. + digitalWriteFast(tx, HIGH); + pinModeFast(tx, OUTPUT); + _transmitBitMask = digitalPinToBitMask(tx); + _transmitPortRegister = portOutputRegister(digitalPinToPort(tx)); } -size_t SoftwareSerialTX::write(const uint8_t *buffer, size_t size) -{ - size_t n = 0; - while (size--) { - if (write(*buffer++)) n++; - else break; - } - return n; +size_t SoftwareSerialTX::write(const uint8_t *buffer, size_t size) { + size_t n = 0; + while (size--) { + if (write(*buffer++)) + n++; + else + break; + } + return n; } #else #error unsupported platform diff --git a/JK-BMSToPylontechCAN/SoftwareSerialTX.h b/JK-BMSToPylontechCAN/SoftwareSerialTX.h index 4e67e8e..0c2f75a 100644 --- a/JK-BMSToPylontechCAN/SoftwareSerialTX.h +++ b/JK-BMSToPylontechCAN/SoftwareSerialTX.h @@ -1,5 +1,5 @@ /* -SoftwareSerialTX.h (from SoftSerial.h) - +SoftwareSerialTX.h (from SoftSerial.h) - Multi-instance software serial library for Arduino/Wiring -- Transmit-only imoplementation -- reduce footprint in code memory and RAM compared to SoftwareSerial @@ -31,7 +31,7 @@ The latest version of this library can always be found at class SoftwareSerialTX { -private: +public: // per object data uint8_t _transmitBitMask; volatile uint8_t *_transmitPortRegister; @@ -45,8 +45,8 @@ class SoftwareSerialTX static uint16_t subtract_cap(uint16_t num, uint16_t sub); #endif -public: // public methods + SoftwareSerialTX(); SoftwareSerialTX(uint8_t transmitPin); void begin(long speed); size_t write(uint8_t byte); diff --git a/README.md b/README.md index fb4bba6..ba34a6b 100644 --- a/README.md +++ b/README.md @@ -31,6 +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). - 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. @@ -41,6 +42,8 @@ The JK-BMS RS485 data (e.g. at connector GPS) are provided as RS232 TTL with 115 - SOC graph output for Arduino Serial Plotter at startup and Capacity Statistics page. Clear data on long press. - The voltage in the SOC graph is corrected by the automatically computed ESR to get a smoother voltage curve. +
+ **If the Aduino IDE complains about more than 100% of program storage space, burn the Uno Bootloader on your Nano, if not already done, and select the Uno as board. The Arduino Nano board definition has a [wrong "upload.maximum_size" value](https://github.com/arduino/ArduinoCore-avr/pull/546).**
Look [here](https://github.com/ArminJo/JK-BMSToPylontechCAN/blob/main/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino#L139) for options to reduce program size / add optional features. @@ -111,6 +114,13 @@ 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. + +[![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) + +
# Example on Wokwi Also usable as connection schematic. @@ -334,6 +344,9 @@ This program uses the following libraries, which are already included in this re - Growatt SPH6000 # Revision History +### Version 4.0.0 +- JK_BMS communication and print functions are now contained in the JK_BMS class. + ### Version 3.2.1 - New macro ENABLE_OVER_AND_UNDER_VOLTAGE_WARNING_ON_LCD.