diff --git a/.github/workflows/TestCompile.yml b/.github/workflows/TestCompile.yml index f4a6123..e96067c 100644 --- a/.github/workflows/TestCompile.yml +++ b/.github/workflows/TestCompile.yml @@ -31,6 +31,7 @@ jobs: - arduino:avr:uno|LOCAL_DEBUG - arduino:avr:uno|DISPLAY_ALWAYS_ON - arduino:avr:uno|USE_NO_LCD + - MightyCore:avr:644 include: - arduino-boards-fqbn: arduino:avr:uno|STANDALONE_TEST @@ -49,6 +50,11 @@ jobs: build-properties: All: -DUSE_NO_LCD + - arduino-boards-fqbn: MightyCore:avr:644 + platform-url: https://mcudude.github.io/MightyCore/package_MCUdude_MightyCore_index.json + build-properties: + All: -DANDRES_644_BOARD + steps: - name: Checkout uses: actions/checkout@master @@ -57,4 +63,6 @@ jobs: uses: ArminJo/arduino-test-compile@master with: # required-libraries: EasyButtonAtInt01,SoftI2CMaster + arduino-board-fqbn: ${{ matrix.arduino-boards-fqbn }} + platform-url: ${{ matrix.platform-url }} build-properties: ${{ toJson(matrix.build-properties) }} \ No newline at end of file diff --git a/JK-BMSToPylontechCAN/ADCUtils.h b/JK-BMSToPylontechCAN/ADCUtils.h new file mode 100644 index 0000000..c406217 --- /dev/null +++ b/JK-BMSToPylontechCAN/ADCUtils.h @@ -0,0 +1,234 @@ +/* + * ADCUtils.h + * + * Copyright (C) 2016-2022 Armin Joachimsmeyer + * Email: armin.joachimsmeyer@gmail.com + * + * This file is part of Arduino-Utils https://github.com/ArminJo/Arduino-Utils. + * + * ArduinoUtils is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef _ADC_UTILS_H +#define _ADC_UTILS_H + +#include + +#if defined(__AVR__) && defined(ADCSRA) && defined(ADATE) && (!defined(__AVR_ATmega4809__)) +#define ADC_UTILS_ARE_AVAILABLE + +// PRESCALE4 => 13 * 4 = 52 microseconds per ADC conversion at 1 MHz Clock => 19,2 kHz +#define ADC_PRESCALE2 1 // 26 microseconds per ADC conversion at 1 MHz +#define ADC_PRESCALE4 2 // 52 microseconds per ADC conversion at 1 MHz +// PRESCALE8 => 13 * 8 = 104 microseconds per ADC sample at 1 MHz Clock => 9,6 kHz +#define ADC_PRESCALE8 3 // 104 microseconds per ADC conversion at 1 MHz +#define ADC_PRESCALE16 4 // 13/208 microseconds per ADC conversion at 16/1 MHz - degradations in linearity at 16 MHz +#define ADC_PRESCALE32 5 // 26/416 microseconds per ADC conversion at 16/1 MHz - very good linearity at 16 MHz +#define ADC_PRESCALE64 6 // 52 microseconds per ADC conversion at 16 MHz +#define ADC_PRESCALE128 7 // 104 microseconds per ADC conversion at 16 MHz --- Arduino default + +// definitions for 0.1 ms conversion time +#if (F_CPU == 1000000) +#define ADC_PRESCALE ADC_PRESCALE8 +#elif (F_CPU == 8000000) +#define ADC_PRESCALE ADC_PRESCALE64 +#elif (F_CPU == 16000000) +#define ADC_PRESCALE ADC_PRESCALE128 +#endif + +/* + * Reference shift values are complicated for ATtinyX5 since we have the extra register bit REFS2 + * in ATTinyCore, this bit is handled programmatical and therefore the defines are different. + * To keep my library small, I use the changed defines. + * After including this file you can not call the ATTinyCore readAnalog functions reliable, if you specify references other than default! + */ +#if defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) +// defines are for ADCUtils.cpp, they can be used WITHOUT bit reordering +#undef DEFAULT +#undef EXTERNAL +#undef INTERNAL1V1 +#undef INTERNAL +#undef INTERNAL2V56 +#undef INTERNAL2V56_EXTCAP + +#define DEFAULT 0 +#define EXTERNAL 4 +#define INTERNAL1V1 8 +#define INTERNAL INTERNAL1V1 +#define INTERNAL2V56 9 +#define INTERNAL2V56_EXTCAP 13 + +#define SHIFT_VALUE_FOR_REFERENCE REFS2 +#define MASK_FOR_ADC_REFERENCE (_BV(REFS0) | _BV(REFS1) | _BV(REFS2)) +#define MASK_FOR_ADC_CHANNELS (_BV(MUX0) | _BV(MUX1) | _BV(MUX2) | _BV(MUX3)) +#else // AVR_ATtiny85 + +#define SHIFT_VALUE_FOR_REFERENCE REFS0 +#define MASK_FOR_ADC_REFERENCE (_BV(REFS0) | _BV(REFS1)) +#define MASK_FOR_ADC_CHANNELS (_BV(MUX0) | _BV(MUX1) | _BV(MUX2) | _BV(MUX3)) +#endif + +// Temperature channel definitions - 1 LSB / 1 degree Celsius +#if defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) +#define ADC_TEMPERATURE_CHANNEL_MUX 15 +#define ADC_1_1_VOLT_CHANNEL_MUX 12 +#define ADC_GND_CHANNEL_MUX 13 +#define ADC_CHANNEL_MUX_MASK 0x0F + +#elif defined(__AVR_ATtiny87__) || defined(__AVR_ATtiny167__) +#define ADC_ISCR_CHANNEL_MUX 3 +#define ADC_TEMPERATURE_CHANNEL_MUX 11 +#define ADC_1_1_VOLT_CHANNEL_MUX 12 +#define ADC_GND_CHANNEL_MUX 14 +#define ADC_VCC_4TH_CHANNEL_MUX 13 +#define ADC_CHANNEL_MUX_MASK 0x1F + +#elif defined(__AVR_ATmega328P__) +#define ADC_TEMPERATURE_CHANNEL_MUX 8 +#define ADC_1_1_VOLT_CHANNEL_MUX 14 +#define ADC_GND_CHANNEL_MUX 15 +#define ADC_CHANNEL_MUX_MASK 0x0F + +#elif defined(__AVR_ATmega644P__) +#define ADC_TEMPERATURE_CHANNEL_MUX // not existent +#define ADC_1_1_VOLT_CHANNEL_MUX 0x1E +#define ADC_GND_CHANNEL_MUX 0x1F +#define ADC_CHANNEL_MUX_MASK 0x0F + +#elif defined(__AVR_ATmega32U4__) +#define ADC_TEMPERATURE_CHANNEL_MUX 0x27 +#define ADC_1_1_VOLT_CHANNEL_MUX 0x1E +#define ADC_GND_CHANNEL_MUX 0x1F +#define ADC_CHANNEL_MUX_MASK 0x3F + +#elif defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_ATmega1284__) || defined(__AVR_ATmega1284P__) || defined(__AVR_ATmega644__) || defined(__AVR_ATmega644A__) || defined(__AVR_ATmega644P__) || defined(__AVR_ATmega644PA__) +#define ADC_1_1_VOLT_CHANNEL_MUX 0x1E +#define ADC_GND_CHANNEL_MUX 0x1F +#define ADC_CHANNEL_MUX_MASK 0x1F + +#define INTERNAL INTERNAL1V1 + +#else +#error "No temperature channel definitions specified for this AVR CPU" +#endif + +/* + * Thresholds for OVER and UNDER voltage and detection of kind of power supply (USB or Li-ion) + * + * Default values are suitable for Li-ion batteries. + * We normally have voltage drop at the connectors, so the battery voltage is assumed slightly higher, than the Arduino VCC. + * But keep in mind that the ultrasonic distance module HC-SR04 may not work reliable below 3.7 volt. + */ +#if !defined(LI_ION_VCC_UNDERVOLTAGE_THRESHOLD_MILLIVOLT) +#define LI_ION_VCC_UNDERVOLTAGE_THRESHOLD_MILLIVOLT 3400 // Do not stress your battery and we require some power for standby +#endif +#if !defined(LI_ION_VCC_EMERGENCY_UNDERVOLTAGE_THRESHOLD_MILLIVOLT) +#define LI_ION_VCC_EMERGENCY_UNDERVOLTAGE_THRESHOLD_MILLIVOLT 3000 // Many Li-ions are specified down to 3.0 volt +#endif + +#if !defined(VCC_UNDERVOLTAGE_THRESHOLD_MILLIVOLT) +#define VCC_UNDERVOLTAGE_THRESHOLD_MILLIVOLT LI_ION_VCC_UNDERVOLTAGE_THRESHOLD_MILLIVOLT +#endif +#if !defined(VCC_EMERGENCY_UNDERVOLTAGE_THRESHOLD_MILLIVOLT) +#define VCC_EMERGENCY_UNDERVOLTAGE_THRESHOLD_MILLIVOLT LI_ION_VCC_EMERGENCY_UNDERVOLTAGE_THRESHOLD_MILLIVOLT +#endif +#if !defined(VCC_OVERVOLTAGE_THRESHOLD_MILLIVOLT) +#define VCC_OVERVOLTAGE_THRESHOLD_MILLIVOLT 5250 // + 5 % operation voltage +#endif +#if !defined(VCC_EMERGENCY_OVERVOLTAGE_THRESHOLD_MILLIVOLT) +#define VCC_EMERGENCY_OVERVOLTAGE_THRESHOLD_MILLIVOLT 5500 // +10 %. Max recommended operation voltage +#endif +#if !defined(VCC_CHECK_PERIOD_MILLIS) +#define VCC_CHECK_PERIOD_MILLIS 10000L // 10 seconds period of VCC checks +#endif +#if !defined(VCC_UNDERVOLTAGE_CHECKS_BEFORE_STOP) +#define VCC_UNDERVOLTAGE_CHECKS_BEFORE_STOP 6 // Shutdown after 6 times (60 seconds) VCC below VCC_UNDERVOLTAGE_THRESHOLD_MILLIVOLT or 1 time below VCC_EMERGENCY_UNDERVOLTAGE_THRESHOLD_MILLIVOLT +#endif + +#if !defined(VOLTAGE_USB_POWERED_LOWER_THRESHOLD_MILLIVOLT) +#define VOLTAGE_USB_POWERED_LOWER_THRESHOLD_MILLIVOLT 4300 // Assume USB powered above this voltage +#endif + +#if !defined(VOLTAGE_USB_POWERED_UPPER_THRESHOLD_MILLIVOLT) +#define VOLTAGE_USB_POWERED_UPPER_THRESHOLD_MILLIVOLT 4950 // Assume USB powered below this voltage, because of the loss in USB cable. If we have > 4950, we assume to be powered by VIN. +// In contrast to e.g. powered by VIN, which results in almost perfect 5 volt supply +#endif + +extern long sLastVCCCheckMillis; +extern uint8_t sVCCTooLowCounter; + +uint16_t readADCChannel(uint8_t aADCChannelNumber); +uint16_t readADCChannelWithReference(uint8_t aADCChannelNumber, uint8_t aReference); +uint16_t waitAndReadADCChannelWithReference(uint8_t aADCChannelNumber, uint8_t aReference); +uint16_t waitAndReadADCChannelWithReferenceAndRestoreADMUXAndReference(uint8_t aADCChannelNumber, uint8_t aReference); +uint16_t readADCChannelWithOversample(uint8_t aADCChannelNumber, uint8_t aOversampleExponent); +void setADCChannelAndReferenceForNextConversion(uint8_t aADCChannelNumber, uint8_t aReference); +uint16_t readADCChannelWithReferenceOversampleFast(uint8_t aADCChannelNumber, uint8_t aReference, uint8_t aOversampleExponent); +uint32_t readADCChannelMultiSamples(uint8_t aPrescale, uint16_t aNumberOfSamples); +uint16_t readADCChannelMultiSamplesWithReference(uint8_t aADCChannelNumber, uint8_t aReference, uint8_t aNumberOfSamples); +uint32_t readADCChannelMultiSamplesWithReferenceAndPrescaler(uint8_t aADCChannelNumber, uint8_t aReference, uint8_t aPrescale, + uint16_t aNumberOfSamples); +uint16_t readADCChannelWithReferenceMax(uint8_t aADCChannelNumber, uint8_t aReference, uint16_t aNumberOfSamples); +uint16_t readADCChannelWithReferenceMaxMicros(uint8_t aADCChannelNumber, uint8_t aReference, uint16_t aMicrosecondsToAquire); +uint16_t readUntil4ConsecutiveValuesAreEqual(uint8_t aADCChannelNumber, uint8_t aReference, uint8_t aDelay, + uint8_t aAllowedDifference, uint8_t aMaxRetries); + +uint8_t checkAndWaitForReferenceAndChannelToSwitch(uint8_t aADCChannelNumber, uint8_t aReference); + +/* + * readVCC*() functions store the result in sVCCVoltageMillivolt or sVCCVoltage + */ +float getVCCVoltageSimple(void); +void readVCCVoltageSimple(void); +void readVCCVoltageMillivoltSimple(void); +void readVCCVoltage(void); +uint16_t getVCCVoltageMillivolt(void); +void readVCCVoltageMillivolt(void); +uint16_t getVCCVoltageReadingFor1_1VoltReference(void); +uint16_t printVCCVoltageMillivolt(Print *aSerial); +void readAndPrintVCCVoltageMillivolt(Print *aSerial); + +uint16_t getVoltageMillivolt(uint16_t aVCCVoltageMillivolt, uint8_t aADCChannelForVoltageMeasurement); +uint16_t getVoltageMillivolt(uint8_t aADCChannelForVoltageMeasurement); +uint16_t getVoltageMillivoltWith_1_1VoltReference(uint8_t aADCChannelForVoltageMeasurement); +float getCPUTemperatureSimple(void); +float getCPUTemperature(void); +float getTemperature(void) __attribute__ ((deprecated ("Renamed to getCPUTemperature()"))); // deprecated + +bool isVCCUSBPowered(); +bool isVCCUSBPowered(Print *aSerial); +bool isVCCUndervoltageMultipleTimes(); +void resetCounterForVCCUndervoltageMultipleTimes(); +bool isVCCUndervoltage(); +bool isVCCEmergencyUndervoltage(); +bool isVCCOvervoltage(); +bool isVCCOvervoltageSimple(); // Version using readVCCVoltageMillivoltSimple() +bool isVCCTooHighSimple(); // Version not using readVCCVoltageMillivoltSimple() + +#endif // defined(__AVR__) ... + +/* + * Variables and functions defined as dummies to allow for seamless compiling on non AVR platforms + */ +extern float sVCCVoltage; +extern uint16_t sVCCVoltageMillivolt; + +uint16_t readADCChannelWithReferenceOversample(uint8_t aADCChannelNumber, uint8_t aReference, uint8_t aOversampleExponent); + +uint16_t getVCCVoltageMillivoltSimple(void); +float getVCCVoltage(void); +float getCPUTemperature(void); + +#endif // _ADC_UTILS_H diff --git a/JK-BMSToPylontechCAN/AVRUtils.cpp b/JK-BMSToPylontechCAN/AVRUtils.cpp index d99c2fc..2e00b03 100644 --- a/JK-BMSToPylontechCAN/AVRUtils.cpp +++ b/JK-BMSToPylontechCAN/AVRUtils.cpp @@ -4,7 +4,7 @@ * Stack, Ram and Heap utilities. * Sleep utilities. * - * Copyright (C) 2016-2023 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. @@ -212,21 +212,22 @@ void printStackUnusedAndUsedBytesIfChanged(Print *aSerial) { * It ends with the heap and the stack. * * Sample output if stack runs into data: - * Size of Data + BSS, Heap start, Stack end=2041 - * Stack used 20 of 7 - * Currently available Heap=0 + * Size of Data + BSS / Heap start=0x773 | 1907 + * Currently available Heap=7 + * Stack used 100 of 141 */ void printRAMInfo(Print *aSerial) { uint16_t tHeapStart = (uint16_t) getHeapStart(); - aSerial->print(F("Size of Data + BSS, Heap start, Stack end=0x")); + aSerial->print(F("Size of Data + BSS / Heap start=0x")); aSerial->print(tHeapStart - RAMSTART, HEX); aSerial->print(F(" | ")); aSerial->println(tHeapStart - RAMSTART); - printStackUsedBytes(aSerial); - aSerial->print(F("Currently available Heap=")); aSerial->println(getCurrentAvailableHeap()); + + printStackUsedBytes(aSerial); + } void printCurrentFreeHeap(Print *aSerial) { diff --git a/JK-BMSToPylontechCAN/EasyButtonAtInt01.h b/JK-BMSToPylontechCAN/EasyButtonAtInt01.h index 5eb880f..0de2514 100644 --- a/JK-BMSToPylontechCAN/EasyButtonAtInt01.h +++ b/JK-BMSToPylontechCAN/EasyButtonAtInt01.h @@ -1,5 +1,5 @@ /* - * EasyButtonAtInt01.hpp + * EasyButtonAtInt01.h * * Arduino library for handling push buttons connected between ground and INT0 and / or INT1 pin. * INT0 and INT1 are connected to Pin 2 / 3 on most Arduinos (ATmega328), to PB6 / PA3 on ATtiny167 and on ATtinyX5 we have only INT0 at PB2. @@ -170,6 +170,16 @@ #define INT1_OUT_PORT (PORTB) # endif // defined(USE_BUTTON_1) +#elif defined(USE_INT2_FOR_BUTTON_0) // Hack for ATmega 644 +# if defined(USE_BUTTON_1) +#error If USE_INT2_FOR_BUTTON_0 is defined, only USE_BUTTON_0 is allowed, USE_BUTTON_1 must be disabled! +# endif +// dirty hack, but INT0 and INT1 are occupied by second USART +#define INT0_PIN 2 // PB2 / INT2 +#define INT0_DDR_PORT (DDRB) +#define INT0_IN_PORT (PINB) +#define INT0_OUT_PORT (PORTB) + #elif defined(__AVR_ATtiny87__) || defined(__AVR_ATtiny167__) // from here we use only ATtinyCore / PAx / PBx numbers, since on Digispark board and core library there is used a strange enumeration of pins #define INT0_PIN 14 // PB6 / INT0 is connected to USB+ on DigisparkPro boards and labeled with 3 (D3) diff --git a/JK-BMSToPylontechCAN/EasyButtonAtInt01.hpp b/JK-BMSToPylontechCAN/EasyButtonAtInt01.hpp index 8b5295d..d0ee2b7 100644 --- a/JK-BMSToPylontechCAN/EasyButtonAtInt01.hpp +++ b/JK-BMSToPylontechCAN/EasyButtonAtInt01.hpp @@ -222,6 +222,12 @@ void EasyButton::init(bool aIsButtonAtINT0) { sPointerToButton0ForISR = this; # if defined(USE_ATTACH_INTERRUPT) attachInterrupt(digitalPinToInterrupt(INT0_PIN), &handleINT0Interrupt, CHANGE); + +# elif defined(USE_INT2_FOR_BUTTON_0) + EICRA |= _BV(ISC20); // interrupt on any logical change + EIFR |= _BV(INTF2);// clear interrupt bit + EIMSK |= _BV(INT2);// enable interrupt on next change + # else EICRA |= _BV(ISC00); // interrupt on any logical change EIFR |= _BV(INTF0);// clear interrupt bit @@ -722,8 +728,8 @@ void __attribute__ ((weak)) handleINT1Interrupt() { // ISR for PIN PD2 // Cannot make the vector itself weak, since the vector table is already filled by weak vectors resulting in ignoring my weak one:-( //ISR(INT0_vect, __attribute__ ((weak))) { -# if defined(USE_BUTTON_0) -ISR(INT0_vect) { +# if defined(USE_INT2_FOR_BUTTON_0) +ISR(INT2_vect) { # if defined(MEASURE_EASY_BUTTON_INTERRUPT_TIMING) digitalWriteFast(INTERRUPT_TIMING_OUTPUT_PIN, HIGH); # endif @@ -732,6 +738,18 @@ ISR(INT0_vect) { digitalWriteFast(INTERRUPT_TIMING_OUTPUT_PIN, LOW); # endif } +# else +# if defined(USE_BUTTON_0) +ISR(INT0_vect) { +# if defined(MEASURE_EASY_BUTTON_INTERRUPT_TIMING) + digitalWriteFast(INTERRUPT_TIMING_OUTPUT_PIN, HIGH); +# endif + handleINT0Interrupt(); +# if defined(MEASURE_EASY_BUTTON_INTERRUPT_TIMING) + digitalWriteFast(INTERRUPT_TIMING_OUTPUT_PIN, LOW); +# endif +} +# endif # endif # if defined(USE_BUTTON_1) diff --git a/JK-BMSToPylontechCAN/JK-BMS.h b/JK-BMSToPylontechCAN/JK-BMS.h index 8729a46..d44a459 100644 --- a/JK-BMSToPylontechCAN/JK-BMS.h +++ b/JK-BMSToPylontechCAN/JK-BMS.h @@ -3,6 +3,14 @@ * * Definitions of 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. + * 2. JKLastReplyStruct - copy of SOC, Uptime, Alarm and Status flags of last reply to detect changes. + * 3. JKConvertedCellInfoStruct - including statistics (min, max, average etc.) for print and LCD usage. + * 4. JKComputedDataStruct - swapped and computed data based on JKReplyStruct content. + * 5. JKLastPrintedDataStruct - part of last JKComputedDataStruct to detect changes. + * 6. CellStatisticsStruct - for minimum and maximum cell voltages statistics. + * * Copyright (C) 2023-2024 Armin Joachimsmeyer * Email: armin.joachimsmeyer@gmail.com * @@ -124,12 +132,16 @@ void fillJKConvertedCellInfo(); * Arrays of counters, which count the times, a cell has minimal or maximal voltage * To identify runaway cells */ -extern uint16_t CellMinimumArray[MAXIMUM_NUMBER_OF_CELLS]; -extern uint16_t CellMaximumArray[MAXIMUM_NUMBER_OF_CELLS]; -extern uint8_t CellMinimumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; -extern uint8_t CellMaximumPercentageArray[MAXIMUM_NUMBER_OF_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() +}; + #define MINIMUM_BALANCING_COUNT_FOR_DISPLAY 60 // 120 seconds / 2 minutes of balancing -extern uint32_t sBalancingCount; // Count of active balancing in SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS (2 seconds) units #endif // NO_CELL_STATISTICS #define JK_BMS_FRAME_HEADER_LENGTH 11 @@ -177,8 +189,8 @@ struct JKComputedDataStruct { float BatteryVoltageFloat; // Volt int16_t Battery10MilliAmpere; // Charging is positive discharging is negative float BatteryLoadCurrentFloat; // Ampere - int32_t BatteryCapacityAccumulator10MilliAmpere; // 500 Ah = 180,000,000 10MilliAmpereSeconds int16_t BatteryLoadPower; // Watt Computed value, Charging is positive discharging is negative + int32_t BatteryCapacityAccumulator10MilliAmpere; // 500 Ah = 180,000,000 10MilliAmpereSeconds 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 diff --git a/JK-BMSToPylontechCAN/JK-BMS.hpp b/JK-BMSToPylontechCAN/JK-BMS.hpp index 13bd58e..c259824 100644 --- a/JK-BMSToPylontechCAN/JK-BMS.hpp +++ b/JK-BMSToPylontechCAN/JK-BMS.hpp @@ -3,6 +3,13 @@ * * Functions to read, convert and print JK-BMS data * + * We use 6 Structures: + * 1. JKReplyStruct - the main reply structure, containing raw BMS reply data Big Endian, which must be swapped. + * 2. JKLastReplyStruct - copy of SOC, Uptime, Alarm and Status flags of last reply to detect changes. + * 3. JKConvertedCellInfoStruct - including statistics (min, max, average etc.) for print and LCD usage. + * 4. JKComputedDataStruct - swapped and computed data based on JKReplyStruct content. + * 5. JKLastPrintedDataStruct - part of last JKComputedDataStruct to detect changes. + * 6. CellStatisticsStruct - for minimum and maximum cell voltages statistics. * * Copyright (C) 2023-2024 Armin Joachimsmeyer * Email: armin.joachimsmeyer@gmail.com @@ -66,16 +73,7 @@ char sLastUpTimeHourCharacter; // For setting sUpTimeStringHourHasC JKConvertedCellInfoStruct JKConvertedCellInfo; // The converted little endian cell voltage data #if !defined(NO_CELL_STATISTICS) -/* - * Arrays of counters, which count the times, a cell has minimal or maximal voltage - * To identify runaway cells - */ -uint16_t CellMinimumArray[MAXIMUM_NUMBER_OF_CELLS]; -uint16_t CellMaximumArray[MAXIMUM_NUMBER_OF_CELLS]; -uint8_t CellMinimumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; -uint8_t CellMaximumPercentageArray[MAXIMUM_NUMBER_OF_CELLS]; -uint32_t sBalancingCount; // Count of active balancing in SECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS (2 seconds) units -uint32_t sLastPrintedBalancingCount; +struct CellStatisticsStruct CellStatistics; #endif //NO_CELL_STATISTICS /* @@ -361,7 +359,6 @@ void myPrintlnSwap(const __FlashStringHelper *aPGMString, uint32_t a32BitValue) Serial.println(swap(a32BitValue)); } - /* * Convert the big endian cell voltage data from JKReplyFrameBuffer to little endian data in JKConvertedCellInfo * and compute minimum, maximum, delta, and average @@ -416,12 +413,12 @@ void fillJKConvertedCellInfo() { if (JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt == tMinimumMillivolt) { JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween = VOLTAGE_IS_MINIMUM; if (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { - CellMinimumArray[i]++; // count for statistics + 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) { - CellMaximumArray[i]++; + CellStatistics.CellMaximumArray[i]++; } } else { JKConvertedCellInfo.CellInfoStructArray[i].VoltageIsMinMaxOrBetween = VOLTAGE_IS_BETWEEN_MINIMUM_AND_MAXIMUM; @@ -439,7 +436,7 @@ void fillJKConvertedCellInfo() { /* * After 43200 counts (a whole day being the minimum / maximum) we do scaling */ - uint16_t tCellStatisticsCount = CellMinimumArray[i]; + uint16_t tCellStatisticsCount = CellStatistics.CellMinimumArray[i]; tCellStatisticsSum += tCellStatisticsCount; if (tCellStatisticsCount > (60UL * 60UL * 24UL * 1000UL / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS)) { /* @@ -452,7 +449,7 @@ void fillJKConvertedCellInfo() { // Here, we demand 2 minutes of balancing as minimum if (tCellStatisticsSum > 60) { for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { - CellMinimumPercentageArray[i] = ((uint32_t) (CellMinimumArray[i] * 100UL)) / tCellStatisticsSum; + CellStatistics.CellMinimumPercentageArray[i] = ((uint32_t) (CellStatistics.CellMinimumArray[i] * 100UL)) / tCellStatisticsSum; } } @@ -462,7 +459,7 @@ void fillJKConvertedCellInfo() { */ Serial.println(F("Do scaling of minimum counts")); for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { - CellMinimumArray[i] = CellMinimumArray[i] / 2; + CellStatistics.CellMinimumArray[i] = CellStatistics.CellMinimumArray[i] / 2; } } @@ -475,7 +472,7 @@ void fillJKConvertedCellInfo() { /* * After 43200 counts (a whole day being the minimum / maximum) we do scaling */ - uint16_t tCellStatisticsCount = CellMaximumArray[i]; + uint16_t tCellStatisticsCount = CellStatistics.CellMaximumArray[i]; tCellStatisticsSum += tCellStatisticsCount; if (tCellStatisticsCount > (60UL * 60UL * 24UL * 1000UL / MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS)) { /* @@ -488,7 +485,7 @@ void fillJKConvertedCellInfo() { // Here, we demand 2 minutes of balancing as minimum if (tCellStatisticsSum > 60) { for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { - CellMaximumPercentageArray[i] = ((uint32_t) (CellMaximumArray[i] * 100UL)) / tCellStatisticsSum; + CellStatistics.CellMaximumPercentageArray[i] = ((uint32_t) (CellStatistics.CellMaximumArray[i] * 100UL)) / tCellStatisticsSum; } } if (tDoDaylyScaling) { @@ -497,7 +494,7 @@ void fillJKConvertedCellInfo() { */ Serial.println(F("Do scaling of maximum counts")); for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { - CellMaximumArray[i] = CellMaximumArray[i] / 2; + CellStatistics.CellMaximumArray[i] = CellStatistics.CellMaximumArray[i] / 2; } } #endif // NO_CELL_STATISTICS @@ -533,7 +530,7 @@ void printJKCellStatisticsInfo() { if (i != 0 && (i % 8) == 0) { Serial.println(); } - sprintf_P(tStringBuffer, PSTR("%2u=%2u %% |%5u, "), i + 1, CellMinimumPercentageArray[i], CellMinimumArray[i]); + sprintf_P(tStringBuffer, PSTR("%2u=%2u %% |%5u, "), i + 1, CellStatistics.CellMinimumPercentageArray[i], CellStatistics.CellMinimumArray[i]); Serial.print(tStringBuffer); } Serial.println(); @@ -543,7 +540,7 @@ void printJKCellStatisticsInfo() { if (i != 0 && (i % 8) == 0) { Serial.println(); } - sprintf_P(tStringBuffer, PSTR("%2u=%2u %% |%5u, "), i + 1, CellMaximumPercentageArray[i], CellMaximumArray[i]); + sprintf_P(tStringBuffer, PSTR("%2u=%2u %% |%5u, "), i + 1, CellStatistics.CellMaximumPercentageArray[i], CellStatistics.CellMaximumArray[i]); Serial.print(tStringBuffer); } Serial.println(); @@ -553,7 +550,7 @@ void printJKCellStatisticsInfo() { #endif // NO_CELL_STATISTICS -void initializeComputedData(){ +void initializeComputedData() { // Initialize capacity accumulator with sensible value JKComputedData.BatteryCapacityAccumulator10MilliAmpere = (AMPERE_HOUR_AS_ACCUMULATOR_10_MILLIAMPERE / 100) * sJKFAllReplyPointer->SOCPercent * JKComputedData.TotalCapacityAmpereHour; @@ -616,12 +613,12 @@ void fillJKComputedData() { #if !defined(NO_CELL_STATISTICS) /* - * Increment sBalancingCount and fill sBalancingTimeString + * Increment BalancingCount and fill sBalancingTimeString */ if (sJKFAllReplyPointer->BMSStatus.StatusBits.BalancerActive) { - sBalancingCount++; - sprintf_P(sBalancingTimeString, PSTR("%3uD%02uH%02uM"), (uint16_t) (sBalancingCount / (60 * 24 * 30UL)), - (uint16_t) ((sBalancingCount / (60 * 30)) % 24), (uint16_t) (sBalancingCount / 30) % 60); + CellStatistics.BalancingCount++; + sprintf_P(sBalancingTimeString, PSTR("%3uD%02uH%02uM"), (uint16_t) (CellStatistics.BalancingCount / (60 * 24 * 30UL)), + (uint16_t) ((CellStatistics.BalancingCount / (60 * 30)) % 24), (uint16_t) (CellStatistics.BalancingCount / 30) % 60); } #endif // NO_CELL_STATISTICS } @@ -792,6 +789,17 @@ void printMiscellaneousInfo() { void detectAndPrintAlarmInfo() { JKReplyStruct *tJKFAllReplyPointer = sJKFAllReplyPointer; +#if defined(SUPPRESS_CONSECUTIVE_SAME_ALARMS) + if (tJKFAllReplyPointer->AlarmUnion.AlarmsAsWord == NO_ALARM_WORD_CONTENT) { + sNoAlarmCounter++; // 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 + /* * Do it only once per change */ @@ -799,14 +807,6 @@ void detectAndPrintAlarmInfo() { if (tJKFAllReplyPointer->AlarmUnion.AlarmsAsWord == NO_ALARM_WORD_CONTENT) { Serial.println(F("All alarms are cleared now")); sAlarmJustGetsActive = false; -#if defined(SUPPRESS_CONSECUTIVE_SAME_ALARMS) - sNoAlarmCounter++; // overflow does not 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; - } -#endif } else { #if defined(SUPPRESS_CONSECUTIVE_SAME_ALARMS) sNoAlarmCounter = 0; // reset counter @@ -971,17 +971,17 @@ void printJKDynamicInfo() { /* * Print cell statistics only if balancing count changed and is big enough for reasonable info */ - if (sLastPrintedBalancingCount != sBalancingCount && sBalancingCount > MINIMUM_BALANCING_COUNT_FOR_DISPLAY) { - sLastPrintedBalancingCount = sBalancingCount; + if (CellStatistics.LastPrintedBalancingCount != CellStatistics.BalancingCount && CellStatistics.BalancingCount > MINIMUM_BALANCING_COUNT_FOR_DISPLAY) { + CellStatistics.LastPrintedBalancingCount = CellStatistics.BalancingCount; Serial.println(F("*** CELL STATISTICS ***")); Serial.print(F("Total balancing time=")); - Serial.print(sBalancingCount * (MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS / 1000)); + Serial.print(CellStatistics.BalancingCount * (MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS / 1000)); Serial.print(F(" s -> ")); Serial.print(sBalancingTimeString); // Append seconds char tString[4]; // "03S" is 3 bytes long - sprintf_P(tString, PSTR("%02uS"), (uint16_t) (sBalancingCount % 30) * 2); + sprintf_P(tString, PSTR("%02uS"), (uint16_t) (CellStatistics.BalancingCount % 30) * 2); Serial.println(tString); printJKCellStatisticsInfo(); } diff --git a/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino b/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino index a638e02..9ecff54 100644 --- a/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino +++ b/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino @@ -103,7 +103,7 @@ */ #include -#define VERSION_EXAMPLE "3.1.0" +#define VERSION_EXAMPLE "3.2.0" // For full revision history see https://github.com/ArminJo/JK-BMSToPylontechCAN?tab=readme-ov-file#revision-history #define MILLIS_IN_ONE_SECOND 1000L @@ -128,6 +128,7 @@ #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. #endif +//#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 ENABLE_MONITORING @@ -145,11 +146,11 @@ //#define MONOTORING_PERIOD_FAST // If active, then print CSV line every 2 seconds, else every minute #define MONOTORING_PERIOD_SLOW // If active, then print CSV line every hour, and CSV line every 10 minutes # endif -#else +#elif FLASHEND <= 0x7FFF // for 32k or less #define DISABLE_MONITORING // Disables writing cell and current values CSV data to serial output. Saves 846 bytes program space. - currently activated to save program space. #endif -#if !defined(SERIAL_INFO_PRINT) && !defined(STANDALONE_TEST) +#if !defined(SERIAL_INFO_PRINT) && !defined(STANDALONE_TEST) && FLASHEND <= 0x7FFF #define NO_SERIAL_INFO_PRINT // Disables writing some info to serial output. Saves 974 bytes program space. - currently activated to save program space. #endif @@ -159,7 +160,7 @@ //#define NO_CAPACITY_35F_EXTENSIONS // Disables generating of frame 0x35F for total capacity. This additional frame is no problem for Deye inverters. //#define NO_CAPACITY_379_EXTENSIONS // Disables generating of frame 0x379 for total capacity. This additional frame is no problem for Deye inverters. //#define NO_BYD_LIMITS_373_EXTENSIONS // Disables generating of frame 0x373 for cell limits as sent by BYD battery. See https://github.com/dfch/BydCanProtocol/tree/main. This additional frame is no problem for Deye inverters. -//#define NO_CELL_STATISTICS // Disables generating and display of cell balancing statistics. Saves 16558 bytes program space. +//#define NO_CELL_STATISTICS // Disables generating and display of cell balancing statistics. Saves 1628 bytes program space. //#define NO_ANALYTICS // Disables generating, storing and display of SOC graph for Arduino Serial Plotter. Saves 3882 bytes program space. //#define USE_NO_LCD // Disables the code for the LCD display. Saves 25% program space on a Nano. @@ -179,11 +180,12 @@ * but additionally this sets the right fuse settings which reserve only 0.5 kB program space. * I regularly do this for all Nano boards I have! * - * With the new fuse settings you can disable the DISABLE_MONITORING macro below, - * or just compile and upload the source for an Uno board even if you have connected the Nano board. + * With the new fuse settings you can just compile and upload the source for an Uno board even if you have connected the Nano board. */ -#define DISABLE_MONITORING // activating this macro saves 528 bytes program space +// Save space for an unmodified Nano +# if !defined(NO_CELL_STATISTICS) #define NO_CELL_STATISTICS // No cell values, cell minimum, maximum and percentages. +# endif #endif // sStringBuffer is defined in JK-BMS_LCD.hpp if DISABLE_MONITORING and NO_ANALYTICS are defined @@ -196,12 +198,17 @@ char sStringBuffer[40]; // for "Store computed capacity" line, p /* * Pin layout, may be adapted to your requirements */ +//#define ANDRES_644_BOARD #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. #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(ANDRES_644_BOARD) +#define JK_BMS_TX_PIN 12 +# else #define JK_BMS_TX_PIN 4 +# endif #endif #if defined(USE_NO_COMMUNICATION_STATUS_LEDS) @@ -212,8 +219,13 @@ char sStringBuffer[40]; // for "Store computed capacity" line, p #else // BMS and CAN communication status LEDs # if !defined(BMS_COMMUNICATION_STATUS_LED_PIN) +# if defined(ANDRES_644_BOARD) +#define BMS_COMMUNICATION_STATUS_LED_PIN 14 +#define CAN_COMMUNICATION_STATUS_LED_PIN 15 +# else #define BMS_COMMUNICATION_STATUS_LED_PIN 6 #define CAN_COMMUNICATION_STATUS_LED_PIN 7 +# endif # endif #define TURN_BMS_STATUS_LED_ON digitalWriteFast(BMS_COMMUNICATION_STATUS_LED_PIN, HIGH) #define TURN_BMS_STATUS_LED_OFF digitalWriteFast(BMS_COMMUNICATION_STATUS_LED_PIN, LOW) @@ -227,7 +239,11 @@ char sStringBuffer[40]; // for "Store computed capacity" line, p //#define TIMING_TEST #if defined(TIMING_TEST) -#define TIMING_TEST_PIN 10 +# if defined(ANDRES_644_BOARD) +#define TIMING_TEST_PIN 13 +# else +#define TIMING_TEST_PIN 10 // is SS pin for SPI and must be used as OUTPUT (set by SPI.init())! +# endif #endif /* @@ -236,13 +252,20 @@ char sStringBuffer[40]; // for "Store computed capacity" line, p * SPI: MOSI - 11, MISO - 12, SCK - 13. CS cannot be replaced by constant ground. * I2C: SDA - A4, SCL - A5. */ -#if !defined(SPI_CS_PIN) // Allow override by global symbol +#if defined(ANDRES_644_BOARD) +#define SPI_CS_PIN 4 // !SS Must be specified before #include "MCP2515_TX.hpp" +#define SPI_MOSI_PIN_FOR_INFO 5 // Definition is not used in program, only for documentation. +#define SPI_MISO_PIN_FOR_INFO 6 // Definition is not used in program, only for documentation. +#define SPI_SCK_PIN_FOR_INFO 7 // Definition is not used in program, only for documentation. +#else +# if !defined(SPI_CS_PIN) // Allow override by global symbol #define SPI_CS_PIN 9 // Pin 9 is the default pin for the Arduino CAN bus shield. Alternately you can use pin 10 on this shield. -//#define SPI_CS_PIN 10 // Must be specified before #include "MCP2515_TX.hpp" +//#define SPI_CS_PIN 10 // Must be specified before #include "MCP2515_TX.hpp" #define SPI_MOSI_PIN_FOR_INFO 11 // Definition is not used in program, only for documentation. #define SPI_MISO_PIN_FOR_INFO 12 // Definition is not used in program, only for documentation. #define SPI_SCK_PIN_FOR_INFO 13 // Definition is not used in program, only for documentation. -#endif +# endif +#endif // defined(ANDRES_644_BOARD) /* * Program timing, may be adapted to your requirements @@ -284,6 +307,9 @@ uint8_t sBeepTimeoutCounter = 0; * * Button at INT0 / D2 for switching LCD pages */ +#if defined(ARDUINO_AVR_ATmega644) +#define USE_INT2_FOR_BUTTON_0 +#endif #define USE_BUTTON_0 // Enable code for 1. button at INT0 / D2 #define BUTTON_DEBOUNCING_MILLIS 100 // With this you can adapt to the characteristic of your button. Default is 50. #define NO_BUTTON_RELEASE_CALLBACK // Disables the code for release callback. This saves 2 bytes RAM and 64 bytes program memory. @@ -456,12 +482,20 @@ void setup() { #endif Serial.begin(115200); -#if defined(__AVR_ATmega32U4__) || defined(SERIAL_PORT_USBVIRTUAL) || defined(SERIAL_USB) /*stm32duino*/|| defined(USBCON) /*STM32_stm32*/|| defined(SERIALUSB_PID) || defined(ARDUINO_attiny3217) + while (!Serial) + ; // Wait for Serial to become available. Is optimized away for some cores. + +#if defined(__AVR_ATmega32U4__) || defined(SERIAL_PORT_USBVIRTUAL) || defined(SERIAL_USB) /*stm32duino*/|| defined(USBCON) /*STM32_stm32*/ \ + || defined(SERIALUSB_PID) || defined(ARDUINO_ARCH_RP2040) || defined(ARDUINO_attiny3217) delay(4000); // To be able to connect Serial monitor after reset or power up and before first print out. Do not wait for an attached Serial Monitor! #endif // Just to know which program is running on my Arduino Serial.println(F("START " __FILE__ "\r\nVersion " VERSION_EXAMPLE " from " __DATE__)); +#if defined(ANDRES_644_BOARD) + JK_INFO_PRINTLN(F("Settings are for Andres 644 board")); +#endif + #if defined(DISABLE_MONITORING) JK_INFO_PRINTLN(F("Monitoring disabled")); #else @@ -483,13 +517,21 @@ delay(4000); // To be able to connect Serial monitor after reset or power up and JK_INFO_PRINTLN(sBatteryESRMilliohm); findFirstSOCDataPointIndex(); +#if defined(ANDRES_644_BOARD) + JK_INFO_PRINT(F("EEPROM SOC data start index=")); + JK_INFO_PRINT(SOCDataPointsInfo.ArrayStartIndex); + JK_INFO_PRINT(F(" length=")); + JK_INFO_PRINT(SOCDataPointsInfo.ArrayLength); + JK_INFO_PRINT(F(", even=")); + JK_INFO_PRINTLN(SOCDataPointsInfo.currentlyWritingOnAnEvenPage); +#else DEBUG_PRINT(F("EEPROM SOC data start index=")); DEBUG_PRINT(SOCDataPointsInfo.ArrayStartIndex); DEBUG_PRINT(F(" length=")); DEBUG_PRINT(SOCDataPointsInfo.ArrayLength); DEBUG_PRINT(F(", even=")); DEBUG_PRINTLN(SOCDataPointsInfo.currentlyWritingOnAnEvenPage); - +#endif JK_INFO_PRINTLN(F("Disable ESR compensation pin=" STR(DISABLE_ESR_IN_GRAPH_OUTPUT_PIN))); JK_INFO_PRINT(F("Battery ESR compensation for voltage ")); if (digitalReadFast(DISABLE_ESR_IN_GRAPH_OUTPUT_PIN) == LOW) { @@ -583,7 +625,7 @@ delay(4000); // To be able to connect Serial monitor after reset or power up and sReplyFrameBufferIndex = sizeof(TestJKReplyStatusFrame) - 1; printJKReplyFrameBuffer(); Serial.println(); - processReceivedData(); + processReceivedData(); // sets sCANDataIsInitialized to true printReceivedData(); /* * Copy complete reply and computed values for change determination @@ -693,20 +735,19 @@ void loop() { } #if defined(STANDALONE_TEST) + sResponseFrameBytesAreExpected = false; // No response! sBMSFrameProcessingComplete = true; // for LCD timeout etc. processReceivedData(); // for statistics printBMSDataOnLCD(); // for switching between MAX and MIN display delay(MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS); // do it simple :-) -#if !defined(USE_NO_COMMUNICATION_STATUS_LEDS) +# if !defined(USE_NO_COMMUNICATION_STATUS_LEDS) + // Simulate BMS reading digitalWriteFast(BMS_COMMUNICATION_STATUS_LED_PIN, HIGH); // Turn on status LED. LED is turned off at end of loop. delay(20); // do it simple :-) digitalWriteFast(BMS_COMMUNICATION_STATUS_LED_PIN, LOW); // Turn on status LED. LED is turned off at end of loop. delay(20); // do it simple :-) - digitalWriteFast(CAN_COMMUNICATION_STATUS_LED_PIN, HIGH); // Turn on status LED. LED is turned off at end of loop. - delay(20); // do it simple :-) - digitalWriteFast(CAN_COMMUNICATION_STATUS_LED_PIN, LOW); // Turn on status LED. LED is turned off at end of loop. -#endif +# endif #else /* @@ -756,9 +797,18 @@ void loop() { * Inverter reply every second: 0x305: 00-00-00-00-00-00-00-00 * Do not send, if BMS is starting up, the 0% SOC during this time will trigger a deye error beep. */ +#if defined(TRACE) + Serial.print(F("sCANDataIsInitialized=")); + Serial.print(sCANDataIsInitialized); + Serial.print(F(" BMSIsStarting=")); + Serial.print(JKComputedData.BMSIsStarting); + Serial.print(F(", sResponseFrameBytesAreExpected=")); + Serial.println(sResponseFrameBytesAreExpected); +#endif if (sCANDataIsInitialized && !JKComputedData.BMSIsStarting && !sResponseFrameBytesAreExpected && millis() - sMillisOfLastCANFrameSent >= MILLISECONDS_BETWEEN_CAN_FRAME_SEND) { sMillisOfLastCANFrameSent = millis(); + TURN_CAN_STATUS_LED_ON; if (sDebugModeActivated) { Serial.println(F("Send CAN")); @@ -969,7 +1019,7 @@ void printReceivedData() { sStaticInfoWasSent = true; initializeComputedData(); #if !defined(NO_ANALYTICS) - initializeAnaltics(); + initializeAnalytics(); #endif printJKStaticInfo(); } @@ -1055,6 +1105,7 @@ void handleOvervoltage() { } #if !defined(_ADC_UTILS_HPP) +#include "ADCUtils.h" /* * Recommended VCC is 1.8 V to 5.5 V, absolute maximum VCC is 6.0 V. * Check for 5.25 V, because such overvoltage is quite unlikely to happen during regular operation. @@ -1065,16 +1116,16 @@ void handleOvervoltage() { * @return true if overvoltage reached */ bool isVCCTooHighSimple() { - ADMUX = 14 | (DEFAULT << 6); + ADMUX = ADC_1_1_VOLT_CHANNEL_MUX | (DEFAULT << SHIFT_VALUE_FOR_REFERENCE); // ADCSRB = 0; // Only active if ADATE is set to 1. // ADSC-StartConversion ADIF-Reset Interrupt Flag - NOT free running mode - ADCSRA = (_BV(ADEN) | _BV(ADSC) | _BV(ADIF) | 7); // 7 -> 104 microseconds per ADC conversion at 16 MHz --- Arduino default + ADCSRA = (_BV(ADEN) | _BV(ADSC) | _BV(ADIF) | ADC_PRESCALE128); // 128 -> 104 microseconds per ADC conversion at 16 MHz --- Arduino default // wait for single conversion to finish loop_until_bit_is_clear(ADCSRA, ADSC); // Get value uint16_t tRawValue = ADCL | (ADCH << 8); - return tRawValue < 214; + return tRawValue < 1126000 / VCC_OVERVOLTAGE_THRESHOLD_MILLIVOLT; // < 214 } #endif // _ADC_UTILS_HPP diff --git a/JK-BMSToPylontechCAN/JK-BMS_Analytics.h b/JK-BMSToPylontechCAN/JK-BMS_Analytics.h index 777c26b..8fad725 100644 --- a/JK-BMSToPylontechCAN/JK-BMS_Analytics.h +++ b/JK-BMSToPylontechCAN/JK-BMS_Analytics.h @@ -43,11 +43,15 @@ struct SOCDataPointDeltaStruct { int8_t Delta100MilliampereHour; // at a capacity of 320 Ah we have 3.2 Ah per 1% SOC }; // First place of size SOCDataPointDeltaStruct is used for sBatteryESRMilliohm_EEPROM + 3 filler bytes -#define NUMBER_OF_SOC_DATA_POINTS (((E2END + 1) - sizeof(SOCDataPointDeltaStruct)) / sizeof(SOCDataPointDeltaStruct)) // 0xFF +#define NUMBER_OF_SOC_DATA_POINTS (((E2END + 1) - sizeof(SOCDataPointDeltaStruct)) / sizeof(SOCDataPointDeltaStruct)) // 0xFE for 1k EEPROM, 0x1FE for 2kEEPROM struct SOCDataPointsInfoStruct { - uint8_t ArrayStartIndex; // Index of first entry in cyclic SOCDataPointsEEPROMArray, index of next value to be written. - uint16_t ArrayLength; // Length of valid data in Array. Required if not fully written. Maximum is NUMBER_OF_SOC_DATA_POINTS + /* + * Index of next value to be written is ArrayStartIndex + ArrayLength % NUMBER_OF_SOC_DATA_POINTS + * => if array is full i.e. ArrayLength == NUMBER_OF_SOC_DATA_POINTS, index of next value to be written is ArrayStartIndex. + */ + uint16_t ArrayStartIndex; // Index of first data entry in cyclic SOCDataPointsEEPROMArray. Index of next value to be written if ArrayLength == NUMBER_OF_SOC_DATA_POINTS. + uint16_t ArrayLength; // Length of valid data in Array. Required if not fully written. Maximum is NUMBER_OF_SOC_DATA_POINTS bool currentlyWritingOnAnEvenPage; // If true SOC_EVEN_EEPROM_PAGE_INDICATION_BIT is set in SOCPercent. uint16_t NumberOfSamples = 0; // For one sample each 2 seconds, we can store up to 36.4 hours here. long AverageAccumulatorVoltageDifferenceToEmpty10Millivolt = 0; // Serves as accumulator to enable a more smooth graph. @@ -64,7 +68,7 @@ struct SOCDataPointMinMaxStruct { int8_t AverageAmpere; }; -void initializeAnaltics(); +void initializeAnalytics(); void updateEEPROMTo_FF(); void writeSOCData(); void findFirstSOCDataPointIndex(); diff --git a/JK-BMSToPylontechCAN/JK-BMS_Analytics.hpp b/JK-BMSToPylontechCAN/JK-BMS_Analytics.hpp index 24c2a26..c4decf8 100644 --- a/JK-BMSToPylontechCAN/JK-BMS_Analytics.hpp +++ b/JK-BMSToPylontechCAN/JK-BMS_Analytics.hpp @@ -55,9 +55,8 @@ volatile EEMEM uint8_t sFiller3_EEPROM; EEMEM SOCDataPointDeltaStruct SOCDataPointsEEPROMArray[NUMBER_OF_SOC_DATA_POINTS]; // 255 for 1 kB EEPROM SOCDataPointsInfoStruct SOCDataPointsInfo; -void initializeAnaltics(){ - SOCDataPointsInfo.lastWrittenBatteryCapacityAccumulator10Milliampere = - JKComputedData.BatteryCapacityAccumulator10MilliAmpere; +void initializeAnalytics() { + SOCDataPointsInfo.lastWrittenBatteryCapacityAccumulator10Milliampere = JKComputedData.BatteryCapacityAccumulator10MilliAmpere; } /* * Just clear the complete EEPROM @@ -80,18 +79,21 @@ void readBatteryESRfromEEPROM() { } /* + * Assumes that cleared EEPROM is filled with 0xFF * Looks for first 0xFF entry or for a change in the SOC_EVEN_EEPROM_PAGE_INDICATION_BIT stored in SOC value. * Sets SOCDataPointsInfo.ArrayStartIndex and SOCDataPointsInfo.ArrayLength */ void findFirstSOCDataPointIndex() { // Default values - uint8_t tSOCDataPointsArrayStartIndex = 0; // value if buffer was not fully written + uint16_t tSOCDataPointsArrayStartIndex = 0; // value if buffer was not fully written uint16_t tSOCDataPointsArrayLength = NUMBER_OF_SOC_DATA_POINTS; // value if SOC jump was found bool tStartPageIsEvenFlag; - for (uint_fast8_t i = 0; i < NUMBER_OF_SOC_DATA_POINTS - 1; ++i) { + 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")); + DEBUG_PRINTLN(tSOCPercent, HEX); if (tSOCPercent == 0xFF) { // We found an empty entry, so EEPROM was not fully written => SOCDataPointsInfo.ArrayStartIndex is 0 tSOCDataPointsArrayLength = i; @@ -104,19 +106,39 @@ void findFirstSOCDataPointIndex() { SOCDataPointsInfo.currentlyWritingOnAnEvenPage = tPageIsEvenFlag; } else if (tStartPageIsEvenFlag ^ tPageIsEvenFlag) { // Here data page changes from even to odd or vice versa +#if defined(ANDRES_644_BOARD) + JK_INFO_PRINT(F("Found even/odd toggling before index=")); + JK_INFO_PRINTLN(i); +#else DEBUG_PRINT(F("Found even/odd toggling before index=")); - DEBUG_PRINT(i); + DEBUG_PRINTLN(i); +#endif tSOCDataPointsArrayStartIndex = i; break; } } - - DEBUG_PRINT(F(" now even is ")); - DEBUG_PRINTLN(SOCDataPointsInfo.currentlyWritingOnAnEvenPage); SOCDataPointsInfo.ArrayStartIndex = tSOCDataPointsArrayStartIndex; SOCDataPointsInfo.ArrayLength = tSOCDataPointsArrayLength; } +void setESRMilliohmAndPrintDeltas(uint16_t aVoltToEmptyAccumulatedDeltasESR, uint16_t aVoltToEmptyAccumulatedDeltasNewESR, + uint8_t aNewESRMilliohm) { +#if !defined(NO_SERIAL_INFO_PRINT) + Serial.print(F("Delta of ")); + Serial.print(sBatteryESRMilliohm); + Serial.print(F(" mOhm=")); + Serial.print(aVoltToEmptyAccumulatedDeltasESR); + Serial.print(F(", Delta of new ")); + Serial.print(aNewESRMilliohm); + Serial.print(F(" mOhm=")); + Serial.println(aVoltToEmptyAccumulatedDeltasNewESR); +#else + (void) aVoltToEmptyAccumulatedDeltasESR; + (void) aVoltToEmptyAccumulatedDeltasNewESR; +#endif + sBatteryESRMilliohm = aNewESRMilliohm; +} + /* * Read and print SOC EEPROM data for Arduino Plotter * Compute ESR. @@ -160,9 +182,13 @@ void readAndPrintSOCData() { // float tDeltaTimeMinutes = 0; /* - * Print more than 500 data points in order to shift unwanted entries out to left of plotter window + * Print 500 data points in order to shift unwanted entries out to left of plotter window */ - Serial.println(F("Print SOC data with each entry printed twice")); + if (((499 / NUMBER_OF_SOC_DATA_POINTS) + 1) > 1) { + Serial.println(F("Print SOC data with each entry printed twice")); + } else { + Serial.println(F("Print SOC data")); + } // Using a bool variable requires 16 bytes more if (digitalReadFast(DISABLE_ESR_IN_GRAPH_OUTPUT_PIN) == LOW) { Serial.println(F("No battery ESR compensation for voltage")); @@ -284,7 +310,10 @@ void readAndPrintSOCData() { } if (i < SOCDataPointsInfo.ArrayLength - 1) { - for (uint_fast8_t j = 0; j < 2; ++j) { + /* + * ((499 / NUMBER_OF_SOC_DATA_POINTS) + 1) is 2 from 166 to 499 and 1 above + */ + for (uint_fast8_t j = 0; j < ((499 / NUMBER_OF_SOC_DATA_POINTS) + 1); ++j) { Serial.print(tCurrentSOCDataPoint.SOCPercent); Serial.print(' '); Serial.print(tCurrentCapacityAmpereHour); // print capacity in Ah @@ -294,9 +323,10 @@ void readAndPrintSOCData() { Serial.print(tCurrentSOCDataPoint.AverageAmpere); // print ampere Serial.println(F(" 0 0 0 0 0 0")); // to clear unwanted entries from former prints } + } else { // print last entry with caption - for (uint_fast8_t j = 0; j < 2; ++j) { + for (uint_fast8_t j = 0; j < ((499 / NUMBER_OF_SOC_DATA_POINTS) + 1); ++j) { Serial.print(F("SOC=")); Serial.print(tMinimumSOCData.SOCPercent); Serial.print(F("%->")); @@ -352,10 +382,22 @@ void readAndPrintSOCData() { Serial.print(swap(sJKFAllReplyPointer->CellUndervoltageProtectionMillivolt) / 1000.0, 2); Serial.println(F("V:0 _:0 _:0 _:0 _:0 _:0 _:0 _:0 _:0")); } - // If not enough data points, padding to 500 data points to guarantee, that old data is shifted out - for (; i < 249; ++i) { - Serial.println(F(" 0 0 0 0 0 0 0 0 0 0")); - Serial.println(F(" 0 0 0 0 0 0 0 0 0 0")); + + /* + * If not enough data points, padding to 500 data points to guarantee, + * that old data is shifted out of the 500 point Arduino Serial Plotter window + */ + if (((499 / NUMBER_OF_SOC_DATA_POINTS) + 1) > 1) { + for (; i < 249; ++i) { + // For 1 k EEPROM + Serial.println(F(" 0 0 0 0 0 0 0 0 0 0")); + Serial.println(F(" 0 0 0 0 0 0 0 0 0 0")); + } + } else { + for (; i < 499; ++i) { + // For 2 k EEPROM + Serial.println(F(" 0 0 0 0 0 0 0 0 0 0")); + } } } @@ -363,7 +405,7 @@ void readAndPrintSOCData() { tSOCDataPointsArrayIndex = (tSOCDataPointsArrayIndex + 1) % NUMBER_OF_SOC_DATA_POINTS; } - if (digitalReadFast(DISABLE_ESR_IN_GRAPH_OUTPUT_PIN) == LOW) { + if (digitalReadFast(DISABLE_ESR_IN_GRAPH_OUTPUT_PIN) == LOW || SOCDataPointsInfo.ArrayLength < 100) { break; // no automatic ESR computation here } /* @@ -371,21 +413,19 @@ void readAndPrintSOCData() { * then run loop again with the ESR of the smaller value. */ if (tVoltToEmptyAccumulatedDeltasESR - 3 >= tVoltToEmptyAccumulatedDeltasESRPlus1) { - JK_INFO_PRINT(F("Delta of +1=")); - JK_INFO_PRINTLN(tVoltToEmptyAccumulatedDeltasESRPlus1); - sBatteryESRMilliohm++; + setESRMilliohmAndPrintDeltas(tVoltToEmptyAccumulatedDeltasESR, tVoltToEmptyAccumulatedDeltasESRPlus1, + sBatteryESRMilliohm + 1); } else if (tVoltToEmptyAccumulatedDeltasESR - 3 >= tVoltToEmptyAccumulatedDeltasESRMinus1) { - JK_INFO_PRINT(F("Delta of -1=")); - JK_INFO_PRINTLN(tVoltToEmptyAccumulatedDeltasESRPlus1); - sBatteryESRMilliohm--; + setESRMilliohmAndPrintDeltas(tVoltToEmptyAccumulatedDeltasESR, tVoltToEmptyAccumulatedDeltasESRMinus1, + sBatteryESRMilliohm - 1); } else { break; // Current BatteryESR deltas is smaller than BatteryESR + 1 and BatteryESR - 1 deltas. } // This is NOT printed for last graph :-) JK_INFO_PRINT(F("Set new ESR to ")); JK_INFO_PRINTLN(sBatteryESRMilliohm); - }; - eeprom_update_byte(&sBatteryESRMilliohm_EEPROM, sBatteryESRMilliohm); // write final value to eeprom + } + eeprom_update_byte(&sBatteryESRMilliohm_EEPROM, sBatteryESRMilliohm); // Update final ESR value to eeprom } /* @@ -429,8 +469,15 @@ void writeSOCData() { JKComputedData.BatteryCapacityAccumulator10MilliAmpere; } - uint8_t tSOCDataPointsArrayLastWriteIndex = (SOCDataPointsInfo.ArrayStartIndex + SOCDataPointsInfo.ArrayLength - 1) - % NUMBER_OF_SOC_DATA_POINTS; + uint16_t tSOCDataPointsArrayNextWriteIndex = (SOCDataPointsInfo.ArrayStartIndex + + SOCDataPointsInfo.ArrayLength) % NUMBER_OF_SOC_DATA_POINTS; + uint16_t tSOCDataPointsArrayLastWriteIndex; + if (tSOCDataPointsArrayNextWriteIndex == 0) { + tSOCDataPointsArrayLastWriteIndex = NUMBER_OF_SOC_DATA_POINTS - 1; + } else { + tSOCDataPointsArrayLastWriteIndex = tSOCDataPointsArrayNextWriteIndex - 1; + } + auto tLastWrittenSOCPercent = eeprom_read_byte(&SOCDataPointsEEPROMArray[tSOCDataPointsArrayLastWriteIndex].SOCPercent); bool tLastWritingOnAnEvenPage = tLastWrittenSOCPercent & SOC_EVEN_EEPROM_PAGE_INDICATION_BIT; tLastWrittenSOCPercent &= ~SOC_EVEN_EEPROM_PAGE_INDICATION_BIT; @@ -440,14 +487,19 @@ void writeSOCData() { * e.g. capacity delta is already reached from 1% to 1.9% SOC */ bool tExtraCapacityChangedMoreThan1Percent = (tLastWrittenSOCPercent == 100 || tLastWrittenSOCPercent == 0 - || (tLastWrittenSOCPercent == 1 && tCurrentSOCPercent != 1)) && abs( - SOCDataPointsInfo.lastWrittenBatteryCapacityAccumulator10Milliampere - - JKComputedData.BatteryCapacityAccumulator10MilliAmpere) > getOnePercentCapacityAsAccumulator10Milliampere(); + || (tLastWrittenSOCPercent == 1 && tCurrentSOCPercent != 1)) + && abs( + SOCDataPointsInfo.lastWrittenBatteryCapacityAccumulator10Milliampere + - JKComputedData.BatteryCapacityAccumulator10MilliAmpere) + > getOnePercentCapacityAsAccumulator10Milliampere(); if (tExtraCapacityChangedMoreThan1Percent) { JK_INFO_PRINTLN(F("Write data for extra capacity")); } + /* + * Write condition + */ if ((tExtraCapacityChangedMoreThan1Percent) || tCurrentSOCPercent > tLastWrittenSOCPercent || tCurrentSOCPercent < (tLastWrittenSOCPercent - 1)) { @@ -467,7 +519,7 @@ void writeSOCData() { // Array is full, overwrite old start entry SOCDataPointsInfo.ArrayStartIndex++; if (SOCDataPointsInfo.ArrayStartIndex == NUMBER_OF_SOC_DATA_POINTS) { - SOCDataPointsInfo.ArrayStartIndex = 0; // Wrap around (required if NUMBER_OF_SOC_DATA_POINTS != 0x100) + SOCDataPointsInfo.ArrayStartIndex = 0; // Wrap around } } else { // Array is not full, increase length @@ -496,10 +548,12 @@ void writeSOCData() { * Adjust accumulator with the value written to avoid rounding errors. * We can have a residual of up to 18000 (100 mAh) after write */ - int tDelta100MilliampereHour = SOCDataPointsInfo.DeltaAccumulator10Milliampere / (CAPACITY_10_mA_ACCUMULATOR_1_AMPERE_HOUR / 10); // / 18000 + int tDelta100MilliampereHour = SOCDataPointsInfo.DeltaAccumulator10Milliampere + / (CAPACITY_10_mA_ACCUMULATOR_1_AMPERE_HOUR / 10); // / 18000 tDelta100MilliampereHour = constrain(tDelta100MilliampereHour, SCHAR_MIN, SCHAR_MAX); // clip to -128 to 128 tSOCDataPoint.Delta100MilliampereHour = tDelta100MilliampereHour; - SOCDataPointsInfo.DeltaAccumulator10Milliampere -= tDelta100MilliampereHour * (CAPACITY_10_mA_ACCUMULATOR_1_AMPERE_HOUR / 10); + SOCDataPointsInfo.DeltaAccumulator10Milliampere -= tDelta100MilliampereHour + * (CAPACITY_10_mA_ACCUMULATOR_1_AMPERE_HOUR / 10); // compute average volt to empty long tNumberOfSamplesTimes5 = SOCDataPointsInfo.NumberOfSamples * 5L; @@ -515,7 +569,6 @@ void writeSOCData() { /* * Write to eeprom */ - uint8_t tSOCDataPointsArrayNextWriteIndex = (tSOCDataPointsArrayLastWriteIndex + 1) % NUMBER_OF_SOC_DATA_POINTS; eeprom_write_block(&tSOCDataPoint, &SOCDataPointsEEPROMArray[tSOCDataPointsArrayNextWriteIndex], sizeof(tSOCDataPoint)); #if !defined(STANDALONE_TEST) diff --git a/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp b/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp index 3ab3c8d..2400e71 100644 --- a/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp +++ b/JK-BMSToPylontechCAN/JK-BMS_LCD.hpp @@ -499,9 +499,9 @@ void printCellStatisticsOnLCD() { for (uint8_t i = 0; i < tNumberOfCellInfoEntries; ++i) { uint8_t tPercent; if (tDisplayCellMinimumStatistics) { - tPercent = CellMinimumPercentageArray[i]; + tPercent = CellStatistics.CellMinimumPercentageArray[i]; } else { - tPercent = CellMaximumPercentageArray[i]; + tPercent = CellStatistics.CellMaximumPercentageArray[i]; } if (tPercent < 10) { myLCD.print(' '); @@ -548,10 +548,14 @@ void printCellStatisticsOnLCD() { void printCapacityInfoOnLCD() { myLCD.setCursor(0, 0); myLCD.print(F("Print plotter graph")); - myLCD.setCursor(0, 2); - myLCD.print(F("You can clear EEPROM")); - myLCD.setCursor(0, 3); - myLCD.print(F("by long press now")); + if (SOCDataPointsInfo.ArrayLength > 1) { + myLCD.setCursor(0, 1); + myLCD.print(F("You can clear EEPROM")); + myLCD.setCursor(0, 2); + myLCD.print(F("by long press")); + myLCD.setCursor(0, 3); + myLCD.print(F("instead of short one")); + } } #endif @@ -1051,16 +1055,18 @@ void checkButtonPressForLCD() { // EEPROM data not already cleared here myLCD.setCursor(0, 0); myLCD.print(F("Clear EEPROM data in")); - myLCD.setCursor(0, 1); - myLCD.print(F("2 seconds ")); + LCDClearLine(1); + myLCD.print(F("2 seconds")); + LCDClearLine(2); + LCDClearLine(3); delay(1000); if (PageSwitchButtonAtPin2.readDebouncedButtonState() == BUTTON_IS_ACTIVE) { // Check again, if still pressed myLCD.setCursor(0, 1); myLCD.print('1'); delay(1000); // To wait for eventual button release if (PageSwitchButtonAtPin2.readDebouncedButtonState() == BUTTON_IS_ACTIVE) { // Check again, if still pressed - myLCD.setCursor(0, 1); - myLCD.print(F("now ")); // is visible for the time EEPROM needs for erasing (+200 ms) + LCDClearLine(1); + myLCD.print(F("now")); // is visible for the time EEPROM needs for erasing (+200 ms) delay(200); // To wait for eventual button release } } @@ -1242,6 +1248,7 @@ void testLCDPages() { void testBigNumbers() { sLCDDisplayPageNumber = JK_BMS_PAGE_BIG_INFO; + myLCD.clear(); for (int j = 0; j < 3; ++j) { // Test with 100 % and 42 % diff --git a/JK-BMSToPylontechCAN/LongUnion.h b/JK-BMSToPylontechCAN/LongUnion.h index 3559ac2..8f90f61 100644 --- a/JK-BMSToPylontechCAN/LongUnion.h +++ b/JK-BMSToPylontechCAN/LongUnion.h @@ -87,7 +87,7 @@ union LongUnion { struct { WordUnion LowWord; WordUnion HighWord; - } WordUnion; + } TwoWordUnions; uint8_t UBytes[4]; // seems to have the same code size as using struct UByte int8_t Bytes[4]; // Bytes[0] is LowByte uint16_t UWords[2]; @@ -122,7 +122,7 @@ union LongLongUnion { WordUnion MidLowWord; WordUnion MidHighWord; WordUnion HighWord; - } WordUnion; + } FourWordUnions; struct { uint32_t LowLong; uint32_t HighLong; @@ -134,7 +134,7 @@ union LongLongUnion { struct { LongUnion LowLong; LongUnion HighLong; - } LongUnion; + } TwoLongUnions; uint8_t UBytes[8]; // seems to have the same code size as using struct UByte int8_t Bytes[8]; uint16_t UWords[4]; diff --git a/JK-BMSToPylontechCAN/Pylontech_CAN.h b/JK-BMSToPylontechCAN/Pylontech_CAN.h index b215a51..9e5620d 100644 --- a/JK-BMSToPylontechCAN/Pylontech_CAN.h +++ b/JK-BMSToPylontechCAN/Pylontech_CAN.h @@ -25,6 +25,9 @@ * */ +// Based on information in: +// https://www.setfirelabs.com/green-energy/pylontech-can-reading-can-replication + #ifndef _PYLONTECH_CAN_H #define _PYLONTECH_CAN_H @@ -32,30 +35,33 @@ #include "JK-BMS.h" #include "LongUnion.h" -/* LOG: - 7 35E 00 00 00 00 00 00 00 00 08 08 - 6 35C 00 00 00 00 00 00 00 00 08 08 - 5 356 00 00 00 00 0A 50 4E 00 07 07 - 4 355 14 02 74 0E 74 0E CC 01 08 08 - 3 351 0E 00 64 00 00 00 00 00 04 04 - 2 359 02 13 00 00 4A 01 00 00 06 06 - 1 305 C0 00 00 00 00 00 00 00 02 02 - 0 305 50 59 4C 4F 4E 20 20 20 08 08 // ("PYLON") - */ - /* - * Frame ID's + * Frame ID's and sample output without CRC */ -#define PYLON_CAN_NETWORK_ALIVE_MSG_FRAME_ID 0x305 +#define PYLON_CAN_NETWORK_ALIVE_MSG_FRAME_ID 0x305 // All zeros +// CANId=0x305, FrameLength=8, Data=0x21, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 + #define PYLON_CAN_BATTERY_LIMITS_FRAME_ID 0x351 // Battery voltage + current limits -#define PYLON_CAN_BATTERY_SOC_SOH_FRAME_ID 0x355 // State of Health (SOH) / State of Charge (SOC) +// CANId=0x351, FrameLength=8, Data=0x28, 0x2, 0xF4, 0x1, 0x20, 0x3, 0xE0, 0x1 | 0x228->55.2V, 0x1F4->50.0A, 0x320->80.0A 0x1E0-> 48.0V + +#define PYLON_CAN_BATTERY_SOC_SOH_FRAME_ID 0x355 // State of Charge (SOC) / State of Health (SOH) +// CANId=0x355, FrameLength=4, Data=0x3C, 0x0, 0x64, 0x0 | 0x3C->60% SOC, 0x64->100% SOH + #define PYLON_CAN_BATTERY_CURRENT_VALUES_U_I_T_FRAME_ID 0x356 // Voltage / Current / Temperature +// CANId=0x356, FrameLength=6, Data=0x6C, 0x14, 0x4, 0x0, 0xDC, 0x0 | 0x146C->52.28V, 0.4A, 0xDC->22.0C + #define PYLON_CAN_BATTERY_ERROR_WARNINGS_FRAME_ID 0x359 // Protection & Alarm flags +// CANId=0x359, FrameLength=7, Data=0x0, 0x0, 0x0, 0x0, 0x1, 0x50, 0x4E | No alarms + #define PYLON_CAN_BATTERY_CHARGE_REQUEST_FRAME_ID 0x35C // Battery charge request flags -#define PYLON_CAN_BATTERY_MANUFACTURER_FRAME_ID 0x35E // Manufacturer name ("PYLON") +// CANId=0x35C, FrameLength=2, Data=0xC0, 0x0 | 0xC0->Charge and discharge enable + +#define PYLON_CAN_BATTERY_MANUFACTURER_FRAME_ID 0x35E // Manufacturer name +// CANId=0x35E, FrameLength=8, Data=0x50, 0x59, 0x4C, 0x4F, 0x4E, 0x20, 0x20, 0x20 | "PYLON" + #define PYLON_CAN_BATTERY_SMA_CAPACITY_FRAME_ID 0x35F // Capacity for SMA Sunny Island inverters #define BYD_CAN_BATTERY_CELL_LIMITS_FRAME_ID 0x373 // Cell limits info frame from BYD battery -#define PYLON_CAN_BATTERY_LUXPOWER_CAPACITY_FRAME_ID 0x379 // Capacity for Luxpower SNA inverters +#define PYLON_CAN_BATTERY_LUXPOWER_CAPACITY_FRAME_ID 0x379 // Capacity for Luxpower SNA inverters extern struct PylontechCANBatteryLimitsFrame351Struct PylontechCANBatteryLimitsFrame351; extern struct PylontechCANSohSocFrame355Struct PylontechCANSohSocFrame355; @@ -97,10 +103,10 @@ struct PylontechCANAliveFrame305Struct { struct PylontechCANBatteryLimitsFrame351Struct { struct CANFrameInfoStruct CANFrameInfo = { PYLON_CAN_BATTERY_LIMITS_FRAME_ID, 8 }; // 0x351 struct { - int16_t BatteryChargeOvervoltage100Millivolt; // 0 to 750 - int16_t BatteryChargeCurrentLimit100Milliampere; // 0 to 5000 - int16_t BatteryDischargeCurrentLimit100Milliampere; // -5000 to 0 - int16_t BatteryDischarge100Millivolt; // 0 to 65535 // not in documentation + 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 } FrameData; void fillFrame(struct JKReplyStruct *aJKFAllReply) { FrameData.BatteryChargeOvervoltage100Millivolt = JKComputedData.BatteryFullVoltage10Millivolt / 10; @@ -134,9 +140,9 @@ struct PylontechCANSohSocFrame355Struct { struct PylontechCANCurrentValuesFrame356Struct { struct CANFrameInfoStruct CANFrameInfo = { PYLON_CAN_BATTERY_CURRENT_VALUES_U_I_T_FRAME_ID, 6 }; // 0x356 struct { - int16_t Voltage10Millivolt; // 0 to 32767 - int16_t Current100Milliampere; // -2500 to 2500 - int16_t Temperature100Millicelsius; // -500 to 750 + int16_t Voltage10Millivolt; // 0 to 32767 | Average of all + 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 diff --git a/JK-BMSToPylontechCAN/SoftwareSerialTX.cpp b/JK-BMSToPylontechCAN/SoftwareSerialTX.cpp index bc85c66..a07a926 100644 --- a/JK-BMSToPylontechCAN/SoftwareSerialTX.cpp +++ b/JK-BMSToPylontechCAN/SoftwareSerialTX.cpp @@ -25,7 +25,7 @@ The latest version of this library can always be found at */ -#if defined(__AVR_ATmega168__) ||defined(__AVR_ATmega168P__) ||defined(__AVR_ATmega328P__) +#if defined(__AVR_ATmega168__) ||defined(__AVR_ATmega168P__) ||defined(__AVR_ATmega328P__) ||defined(__AVR_ATmega644P__) // // Includes // diff --git a/README.md b/README.md index 800908c..d803f21 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The JK-BMS RS485 data (e.g. at connector GPS) are provided as RS232 TTL with 115 # Features - Protocol converter from the JK-BMS status frame to Pylontech CAN frames. -- Supports sending of total capayity for SMA and Luxpower inverters. +- 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). - Display of BMS information, Cell voltages, statistics and alarms on a locally attached **serial 2004 LCD**. - Page button for switching **5 LCD display pages**. @@ -39,7 +39,7 @@ The JK-BMS RS485 data (e.g. at connector GPS) are provided as RS232 TTL with 115 - Beep on alarm and connection timeouts with selectable timeout. - Serial.print() function is still available for monitoring and debugging. - 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 automatic computed ESR to get a smoother voltage curve. +- 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).**
Enabling the macro `NO_SERIAL_INFO_PRINT` saves program space. @@ -194,12 +194,14 @@ Alternative circuit for VCC lower than 5 volt e.g. for supply by Li-ion battery
# Example schematics and PCB layouts -- EasyEda [schematics](https://easyeda.com/editor#id=0d1a2556b7634c8bbd22e9c0474cd401) and [PCB layout](https://easyeda.com/editor#id=623a04630b8b4449b72bd5462f59e85f) by Ngoc Dang Dinh. +- KiCad8 schematics and PCB layout for ATmega644 compiled with [MightyCore](https://github.com/MCUdude/MightyCore) and 2 BMS connectors by Andé Meier. +[![PCB layout](https://github.com/ArminJo/JK-BMSToPylontechCAN/blob/main/pictures/BMS-CAN_PCB_top_v0.1.png)](https://github.com/dremeier/Arduino-JK-BMS-To-Pylontech-CAN-PCB) + +- EasyEda schematics and PCB layout by Ngoc Dang Dinh. +[![Minimal layout](https://github.com/ArminJo/JK-BMSToPylontechCAN/blob/main/pictures/EasyEda_shematics_by_Ngoc_Dang_Dinh.png)](https://easyeda.com/editor#id=0d1a2556b7634c8bbd22e9c0474cd401) - EasyEda [schematics](https://easyeda.com/editor#id=809cb7e913b5453f9d324c442df66a4e) and [PCB layout](https://easyeda.com/editor#id=005061dbeb414870bc63ab052561ddf4) by rooftopsolarsa/WannaBeSolarSparky from [this](https://github.com/ArminJo/JK-BMSToPylontechCAN/discussions/27) discussion. The status LEDs are missing in the schematic and button2 is no longer required. -### Minimal layout (by Ngoc Dang Dinh) -![Minimal layout](https://github.com/ArminJo/JK-BMSToPylontechCAN/blob/main/pictures/EasyEda_shematics_by_Ngoc_Dang_Dinh.png)
@@ -317,6 +319,10 @@ This program uses the following libraries, which are already included in this re - Growatt SPH6000 # Revision History +### Version 3.2.0 +- Adaption for ATmega644. +- Automatic ESR computation only for more than 100 data points. + ### Version 3.1.0 - Analytics bug fixing. - Analytics graph handling of extra capacity below 0 % and above 100 %. diff --git a/pictures/BMS-CAN_PCB_top_v0.1.png b/pictures/BMS-CAN_PCB_top_v0.1.png new file mode 100644 index 0000000..e10ce3a Binary files /dev/null and b/pictures/BMS-CAN_PCB_top_v0.1.png differ