diff --git a/.github/workflows/TestCompile.yml b/.github/workflows/TestCompile.yml new file mode 100644 index 0000000..2f16e79 --- /dev/null +++ b/.github/workflows/TestCompile.yml @@ -0,0 +1,32 @@ +# TestCompile.yml +# Github workflow script to test compile all examples of an Arduino library repository. +# +# Copyright (C) 2020-2022 Armin Joachimsmeyer +# https://github.com/ArminJo/Github-Actions +# + +# This is the name of the workflow, visible on GitHub UI. +name: TestCompile +on: + workflow_dispatch: # To run it manually + description: 'manual build check' + push: + paths: + - '**.ino' + - '**.cpp' + - '**.hpp' + - '**.h' + - '**TestCompile.yml' +jobs: + build: + name: Test compiling examples for UNO + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@master + + - name: Compile all examples + uses: ArminJo/arduino-test-compile@master +# with: +# required-libraries: EasyButtonAtInt01,SoftI2CMaster +# build-properties: -DCRYSTAL_20MHZ_ASSEMBLED diff --git a/JK-BMSToPylontechCAN/EasyButtonAtInt01.h b/JK-BMSToPylontechCAN/EasyButtonAtInt01.h new file mode 100644 index 0000000..b116765 --- /dev/null +++ b/JK-BMSToPylontechCAN/EasyButtonAtInt01.h @@ -0,0 +1,408 @@ +/* + * EasyButtonAtInt01.hpp + * + * 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. + * The library is totally based on interrupt. + * Debouncing is implemented in a not blocking way! It is merely done by ignoring a button change within the debouncing time. + * So button state is instantly available without debouncing delay! + * + * Usage: + * #define USE_BUTTON_0 + * #include "EasyButtonAtInt01.h" + * EasyButton Button0AtPin2(true); + * The macros INT0_PIN and INT1_PIN are set after the include. + * + * Copyright (C) 2018-2022 Armin Joachimsmeyer + * armin.joachimsmeyer@gmail.com + * + * This file is part of EasyButtonAtInt01 https://github.com/ArminJo/EasyButtonAtInt01. + * + * EasyButtonAtInt01 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 See the 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 _EASY_BUTTON_AT_INT01_H +#define _EASY_BUTTON_AT_INT01_H + +#define VERSION_EASY_BUTTON "3.3.2" +#define VERSION_EASY_BUTTON_MAJOR 3 +#define VERSION_EASY_BUTTON_MINOR 3 +#define VERSION_EASY_BUTTON_PATCH 2 +// The change log is at the bottom of the file + +/* + * Macro to convert 3 version parts into an integer + * To be used in preprocessor comparisons, such as #if VERSION_EASY_BUTTON_HEX >= VERSION_HEX_VALUE(3, 0, 0) + */ +#define VERSION_HEX_VALUE(major, minor, patch) ((major << 16) | (minor << 8) | (patch)) +#define VERSION_EASY_BUTTON_HEX VERSION_HEX_VALUE(VERSION_EASY_BUTTON_MAJOR, VERSION_EASY_BUTTON_MINOR, VERSION_EASY_BUTTON_PATCH) +#if defined(__AVR__) +#include + +/* + * Usage: + * #define USE_BUTTON_0 // Enable code for button at INT0 + * #define USE_BUTTON_1 // Enable code for button at INT1 (PCINT0 for ATtinyX5) + * #include "EasyButtonAtInt01.h" + * EasyButton Button0AtPin2(true); // true -> Button is connected to INT0 + * EasyButton Button0AtPin3(false, &Button3CallbackHandler); // false -> button is not connected to INT0 => connected to INT1 + * ... + * digitalWrite(LED_BUILTIN, Button0AtPin2.ButtonToggleState); + * ... + * + */ + +/* + * Enable this if you buttons are active high. + */ +//#define BUTTON_IS_ACTIVE_HIGH +/* + * Define USE_ATTACH_INTERRUPT to force use of the arduino function attachInterrupt(). + * It is required if you get the error " multiple definition of `__vector_1'" (or `__vector_2'), because another library uses the attachInterrupt() function. + * For one button it needs additional 160 bytes program memory, for 2 buttons it needs additional 88 bytes. + */ +//#define USE_ATTACH_INTERRUPT +// +/* + * You can define your own value if you have buttons which are worse or better than the one I have. + * Since debouncing is not done with blocking wait, reducing this value makes not much sense, except you expect regular short button presses, + * which durations are shorter than BUTTON_DEBOUNCING_MILLIS. + * Press duration below 50 ms are almost impossible to generate by normal button pressing, but they can generated by just hitting the button. + * + * Test your own new value with the DebounceTest example + * + * Analyze the button actual debounce value with defining ANALYZE_MAX_BOUNCING_PERIOD and looking at MaxBouncingPeriodMillis. + * Defining ANALYZE_MAX_BOUNCING_PERIOD computes the maximum bouncing period. + * this is the time between first level change and last bouncing level change during BUTTON_DEBOUNCING_MILLIS + */ +//#define ANALYZE_MAX_BOUNCING_PERIOD +#if !defined(BUTTON_DEBOUNCING_MILLIS) +#define BUTTON_DEBOUNCING_MILLIS 50 // 35 millis measured for my button :-). +#endif + +/* + * Activating this enables save 2 bytes RAM and 64 bytes program memory + */ +//#define NO_BUTTON_RELEASE_CALLBACK +// +/* + * Return values for checkForLongPress() + */ +#define EASY_BUTTON_LONG_PRESS_STILL_POSSIBLE 0 +#define EASY_BUTTON_LONG_PRESS_ABORT 1 // button was released, no long press detection possible +#define EASY_BUTTON_LONG_PRESS_DETECTED 2 + +#define EASY_BUTTON_LONG_PRESS_DEFAULT_MILLIS 400 +#define EASY_BUTTON_DOUBLE_PRESS_DEFAULT_MILLIS 400 + +/* + * This activates LED_BUILTIN as long as button is pressed + */ +//#define BUTTON_LED_FEEDBACK +#if defined(BUTTON_LED_FEEDBACK) +# if !defined(BUTTON_LED_FEEDBACK_PIN) +# if defined(LED_BUILTIN) +# define BUTTON_LED_FEEDBACK_PIN LED_BUILTIN // if not specified, use built in led - pin 13 on Uno board +# else +# error "BUTTON_LED_FEEDBACK is defined but neither BUTTON_LED_FEEDBACK_PIN nor LED_BUILTIN is defined" +# endif +# endif +#endif + +// For external measurement of code timing +//#define MEASURE_EASY_BUTTON_INTERRUPT_TIMING + +#if defined(MEASURE_EASY_BUTTON_INTERRUPT_TIMING) +# if !defined(INTERRUPT_TIMING_OUTPUT_PIN) +#define INTERRUPT_TIMING_OUTPUT_PIN 6 // use pin 6 +//#define INTERRUPT_TIMING_OUTPUT_PIN 12 // use pin 12 +# endif +#endif + +//#define TRACE +#if defined(TRACE) +#warning If using TRACE, the timing of the interrupt service routine changes, e.g. you will see more spikes, than expected! +#endif + +/* + * These defines are here to enable saving of 150 bytes program memory if only one button is needed + */ +//#define USE_BUTTON_0 +//#define USE_BUTTON_1 +#if ! (defined(USE_BUTTON_0) || defined(USE_BUTTON_1)) +#error USE_BUTTON_0 and USE_BUTTON_1 are not defined, please define them or remove the #include "EasyButtonAtInt01.h" +#endif +// Can be be used as parameter +#define BUTTON_AT_INT0 ((bool)true) +#define BUTTON_AT_INT1_OR_PCINT ((bool)false) +/* + * Pin and port definitions for Arduinos + */ +#if defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) +#define INT0_PIN 2 +#define INT0_DDR_PORT (DDRB) +#define INT0_IN_PORT (PINB) +#define INT0_OUT_PORT (PORTB) + +# if defined(USE_BUTTON_1) +# if !defined(INT1_PIN) +#define INT1_PIN 3 +# elif (INT1_PIN != 2) && (INT1_PIN > 5) +#error INT1_PIN (for PCINT0 interrupt) can only be 0,1,3,4,5 +# endif +#define INT1_DDR_PORT (DDRB) +#define INT1_IN_PORT (PINB) +#define INT1_OUT_PORT (PORTB) +# endif // defined(USE_BUTTON_1) + +#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) +#define INT0_DDR_PORT (DDRB) +#define INT0_IN_PORT (PINB) +#define INT0_OUT_PORT (PORTB) + + +# if defined(USE_BUTTON_1) +# if !defined(INT1_PIN) +#define INT1_PIN 3 // PA3 labeled 9 on DigisparkPro boards +# endif // !defined(INT1_PIN) + +// Check for pin range and map digispark to PA pin number +# if defined(ARDUINO_AVR_DIGISPARKPRO) +# if INT1_PIN == 5 +#undef INT1_PIN +#define INT1_PIN 7 // PA7 +# elif INT1_PIN == 6 +#undef INT1_PIN +#define INT1_PIN 0 // PA0 +# elif INT1_PIN == 7 +#undef INT1_PIN +#define INT1_PIN 1 // PA1 +# elif INT1_PIN == 8 +#undef INT1_PIN +#define INT1_PIN 2 // PA2 +# elif INT1_PIN == 9 +#undef INT1_PIN +#define INT1_PIN 3 // PA3 +# elif INT1_PIN == 10 +#undef INT1_PIN +#define INT1_PIN 4 // PA4 +# elif INT1_PIN == 11 +#undef INT1_PIN +#define INT1_PIN 5 // PA5 +# elif INT1_PIN == 12 +#undef INT1_PIN +#define INT1_PIN 6 // PA6 +# else +#error INT1_PIN (for PCINT0 interrupt) can only be 5 to 12 +# endif +# else // defined(ARDUINO_AVR_DIGISPARKPRO) +# if (INT1_PIN > 7) +#error INT1_PIN (for PCINT0 interrupt) can only be 0 to 7 +# endif +# endif // defined(ARDUINO_AVR_DIGISPARKPRO) +#define INT1_DDR_PORT (DDRA) +#define INT1_IN_PORT (PINA) +#define INT1_OUT_PORT (PORTA) +# endif // defined(USE_BUTTON_1) +#else + +// other AVR MCUs +#define INT0_PIN 2 +#define INT0_DDR_PORT (DDRD) +#define INT0_IN_PORT (PIND) +#define INT0_OUT_PORT (PORTD) + +# if defined(USE_BUTTON_1) +# if !defined(INT1_PIN) +#define INT1_PIN 3 +# elif (INT1_PIN > 7) +#error INT1_PIN (for PCINT2 interrupt) can only be Arduino pins 0 to 7 (PD0 to PD7) +# endif +#define INT1_DDR_PORT (DDRD) +#define INT1_IN_PORT (PIND) +#define INT1_OUT_PORT (PORTD) +# endif // defined(USE_BUTTON_1) +#endif // defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) + +#if defined(USE_BUTTON_1) +#define INT1_BIT INT1_PIN +#endif + +#if defined(USE_BUTTON_1) && ((!defined(ISC10)) || ((defined(__AVR_ATtiny87__) || defined(__AVR_ATtiny167__)) && INT1_PIN != 3)) \ + && !defined(INTENTIONALLY_USE_PCI0_FOR_BUTTON1) && !(defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)) +#warning Using PCINT0 interrupt for button 1 +#endif + +#if defined(__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__) +#define EICRA MCUCR +#define EIFR GIFR +#define EIMSK GIMSK +#endif + +#if (INT0_PIN >= 8) +#define INT0_BIT (INT0_PIN - 8) +#else +#define INT0_BIT INT0_PIN +#endif + +class EasyButton { + +public: + + /* + * These constructors are deterministic if only one button is enabled + * If two buttons are enabled they can be taken for the 1. button at INT0 + */ + EasyButton(); + EasyButton(void (*aButtonPressCallback)(bool aButtonToggleState)); +#if !defined(NO_BUTTON_RELEASE_CALLBACK) + EasyButton(void (*aButtonPressCallback)(bool aButtonToggleState), + void (*aButtonReleaseCallback)(bool aButtonToggleState, uint16_t aButtonPressDurationMillis)); +#endif + /* + * These constructors use the first (bool) parameter to decide which button to take. + */ + EasyButton(bool aIsButtonAtINT0); + EasyButton(bool aIsButtonAtINT0, void (*aButtonPressCallback)(bool aButtonToggleState)); +#if !defined(NO_BUTTON_RELEASE_CALLBACK) + EasyButton(bool aIsButtonAtINT0, void (*aButtonPressCallback)(bool aButtonToggleState), + void (*aButtonReleaseCallback)(bool aButtonToggleState, uint16_t aButtonPressDurationMillis)); +#endif + + void init(bool aIsButtonAtINT0); + bool enablePCIInterrupt(); + + /* + * !!! checkForDoublePress() works only reliable if called in button press callback function !!! + */ + bool checkForDoublePress(uint16_t aDoublePressDelayMillis = EASY_BUTTON_DOUBLE_PRESS_DEFAULT_MILLIS); + + bool readButtonState(); + bool readDebouncedButtonState(); + bool updateButtonState(); + uint16_t updateButtonPressDuration(); // Updates the ButtonPressDurationMillis by polling, since this cannot be done by interrupt. + + bool checkForForButtonNotPressedTime(uint16_t aTimeoutMillis); + + //!!! Consider to use button release callback handler and check the ButtonPressDurationMillis + uint8_t checkForLongPress(uint16_t aLongPressThresholdMillis = EASY_BUTTON_LONG_PRESS_DEFAULT_MILLIS); + bool checkForLongPressBlocking(uint16_t aLongPressThresholdMillis = EASY_BUTTON_LONG_PRESS_DEFAULT_MILLIS); + + void handleINT01Interrupts(); // internal use only + + bool LastBounceWasChangeToInactive; // Internal state, reflects actual reading with spikes and bouncing. Negative logic: true / active means button pin is LOW + volatile bool ButtonStateIsActive; // Negative logic: true / active means button pin is LOW. If last press duration < BUTTON_DEBOUNCING_MILLIS it holds wrong value (true instead of false) :-( + volatile bool ButtonToggleState; // Toggle is on press, not on release - initial value is false + + /* + * Flag to enable action only once. Only set to true by library. + * Can be checked and set to false by main program to enable only one action per button press. + */ + volatile bool ButtonStateHasJustChanged; + + /* + * Duration of active state. Is is set at button release. Can in theory not be less than BUTTON_DEBOUNCING_MILLIS. + * By definition, shorter presses are recognized as bouncing. + * To cover this case you can call updateButtonState() from an outside loop which updates the button state in this case. + */ + volatile uint16_t ButtonPressDurationMillis; + + /* + * Milliseconds of last button change, going active or inactive. + * If bouncing occurs the time is not updated with the time of the bouncing. + * So ButtonPressDurationMillis is the complete time and not the time after the last bounce. + */ + volatile unsigned long ButtonLastChangeMillis; + + /* + * If last button change was going inactive, ButtonReleaseMillis contains the same value as ButtonLastChangeMillis + * It is required for double press recognition, which is done when button is active and ButtonLastChangeMillis has just changed. + * Be aware, that the first press after booting may be detected as double press! + * This is because ButtonReleaseMillis is initialized with 0 milliseconds, which is interpreted as the first press happened at the beginning of boot. + */ + volatile unsigned long ButtonReleaseMillis; + +#if defined(ANALYZE_MAX_BOUNCING_PERIOD) + volatile unsigned int MaxBouncingPeriodMillis = 0; // Maximum bouncing period. Time between first level change and last bouncing level change during BUTTON_DEBOUNCING_MILLIS +#endif + + volatile bool isButtonAtINT0; + void (*ButtonPressCallback)(bool aButtonToggleState) = NULL; // If not null, is called on every button press with ButtonToggleState as parameter +#if !defined(NO_BUTTON_RELEASE_CALLBACK) + void (*ButtonReleaseCallback)(bool aButtonToggleState, uint16_t aButtonPressDurationMillis) = NULL; // If not null, is called on every button release with ButtonPressDurationMillis as parameter +#endif + +#if defined(USE_BUTTON_0) + static EasyButton *sPointerToButton0ForISR; +#endif +#if defined(USE_BUTTON_1) + static EasyButton *sPointerToButton1ForISR; +#endif +}; +// end of class definition + +void handleINT0Interrupt(); +void handleINT1Interrupt(); + +/* + * This functions are weak and can be replaced by your own code + */ +#if defined(USE_BUTTON_0) +void __attribute__ ((weak)) handleINT0Interrupt(); +#endif + +#if defined(USE_BUTTON_1) +void __attribute__ ((weak)) handleINT1Interrupt(); +#endif + +#endif // defined(__AVR__) + +/* + * Version 3.3.2 - 9/2022 + * - Added NO_INITIALIZE_IN_CONSTRUCTOR macro to enable late initializing. + * + * Version 3.3.1 - 2/2022 + * - Avoid mistakenly double press detection after boot. + * + * Version 3.3.0 - 9/2021 + * - Renamed EasyButtonAtInt01.cpp.h to EasyButtonAtInt01.hpp. + * + * Version 3.2.0 - 1/2021 + * - Allow button1 on pin 8 to 13 and A0 to A5 for ATmega328. + * + * Version 3.1.0 - 6/2020 + * - 2 sets of constructors, one for only one button used and one for the second button if two buttons used. + * - Map pin numbers for Digispark pro boards, for use with with digispark library. + * + * Version 3.0.0 - 5/2020 + * - Added button release handler and adapted examples. + * - Revoke change for "only one true result per press for checkForLongPressBlocking()". It is superseded by button release handler. + * - Support buttons which are active high by defining BUTTON_IS_ACTIVE_HIGH. + * - Improved detection of maximum bouncing period used in DebounceTest. + * + * Version 2.1.0 - 5/2020 + * - Avoid 1 ms delay for checkForLongPressBlocking() if button is not pressed. + * - Only one true result per press for checkForLongPressBlocking(). + * + * Version 2.0.0 - 1/2020 + * - Ported to ATtinyX5 and ATiny167. + * - Support also PinChangeInterrupt for button 1 on Pin PA0 to PA7 for ATtiniy87/167. + * - Long press detection support. + * - Double press detection support. + * - Renamed to EasyButtonAtInt01.hpp + */ + +#endif // _EASY_BUTTON_AT_INT01_H diff --git a/JK-BMSToPylontechCAN/EasyButtonAtInt01.hpp b/JK-BMSToPylontechCAN/EasyButtonAtInt01.hpp new file mode 100644 index 0000000..f8fef09 --- /dev/null +++ b/JK-BMSToPylontechCAN/EasyButtonAtInt01.hpp @@ -0,0 +1,740 @@ +/* + * EasyButtonAtInt01.hpp + * + * This file can be directly configured and included in one source. + * Include EasyButtonAtInt01.h file if you need the declarations in a second source file. + * + * 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. + * The library is totally based on interrupt. + * Debouncing is implemented in a not blocking way! It is merely done by ignoring a button change within the debouncing time. + * So button state is instantly available without debouncing delay! + * + * Usage: + * #define USE_BUTTON_0 + * #include "EasyButtonAtInt01.hpp" + * EasyButton Button0AtPin2(true); + * + * Copyright (C) 2018-2022 Armin Joachimsmeyer + * armin.joachimsmeyer@gmail.com + * + * This file is part of EasyButtonAtInt01 https://github.com/ArminJo/EasyButtonAtInt01. + * + * EasyButtonAtInt01 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 See the 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 . + */ + +/* + * This library can be configured at compile time by the following options / macros: + * For more details see: https://github.com/ArminJo/EasyButtonAtInt01#compile-options--macros-for-this-library + * + * - USE_BUTTON_0 Enables code for button at INT0 (pin2 on 328P, PB6 on ATtiny167, PB2 on ATtinyX5). + * - USE_BUTTON_1 Enables code for button at INT1 (pin3 on 328P, PA3 on ATtiny167, PCINT0 / PCx for ATtinyX5). + * - INT1_PIN It overrides the usage of pin at the processors INT1 pin. Thus, it is the pin number of the pin for button 1 to use with Pin Change Interrupts. + * - NO_INITIALIZE_IN_CONSTRUCTOR Disables the auto initializing in all constructors without the aIsButtonAtINT0 parameter. + * - BUTTON_IS_ACTIVE_HIGH Enable this if you buttons are active high. + * - USE_ATTACH_INTERRUPT This forces use of the arduino function attachInterrupt(). It is required if you get the error "multiple definition of __vector_1". + * - NO_BUTTON_RELEASE_CALLBACK Disables the code for release callback. This saves 2 bytes RAM and 64 bytes program memory. + * - BUTTON_DEBOUNCING_MILLIS With this you can adapt to the characteristic of your button. + * - ANALYZE_MAX_BOUNCING_PERIOD Analyze the buttons actual debounce value. + * - BUTTON_LED_FEEDBACK This activates LED_BUILTIN as long as button is pressed. + * - BUTTON_LED_FEEDBACK_PIN The pin to use for button LED feedback. + * + * The macros INT0_PIN and INT1_PIN are set after the include. + */ + +#ifndef _EASY_BUTTON_AT_INT01_HPP +#define _EASY_BUTTON_AT_INT01_HPP + +#if defined(__AVR__) +#include +#include "EasyButtonAtInt01.h" + +/* + * Usage: + * #define USE_BUTTON_0 // Enables code for button at INT0 (pin2 on 328P, PB6 on ATtiny167, PB2 on ATtinyX5) + * #define USE_BUTTON_1 // Enables code for button at INT1 (pin3 on 328P, PA3 on ATtiny167, PCINT0 / PCx for ATtinyX5) + * #include "EasyButtonAtInt01.hpp" + * EasyButton Button0AtPin2(true); // true -> Button is connected to INT0 + * EasyButton Button0AtPin3(false, &Button3CallbackHandler); // false -> button is not connected to INT0 => connected to INT1 + * ... + * digitalWrite(LED_BUILTIN, Button0AtPin2.ButtonToggleState); // The value at the first call after first press is true + * ... + * + */ + +// For external measurement of code timing +//#define MEASURE_EASY_BUTTON_INTERRUPT_TIMING +#if defined(MEASURE_EASY_BUTTON_INTERRUPT_TIMING) || defined(BUTTON_LED_FEEDBACK) +#include "digitalWriteFast.h" +#endif + +#if defined(USE_BUTTON_0) +EasyButton *EasyButton::sPointerToButton0ForISR; +#endif +#if defined(USE_BUTTON_1) +EasyButton *EasyButton::sPointerToButton1ForISR; +#endif +#if !defined(USE_BUTTON_0) && !defined(USE_BUTTON_1) +#error One of USE_BUTTON_0 or USE_BUTTON_1 must be defined +#endif + +// The eclipse formatter has problems with // comments in undefined code blocks +// !!! Must be without comment and closed by @formatter:on +// @formatter:off + +/* + * These constructors are deterministic if only one button is enabled + * If both buttons are enabled, it initializes the 1. button (USE_BUTTON_0), + * so the second button must be enabled manually or by using a constructor with the parameter aIsButtonAtINT0. + */ +EasyButton::EasyButton() { // @suppress("Class members should be properly initialized") +#if !defined(NO_INITIALIZE_IN_CONSTRUCTOR) +# if defined(USE_BUTTON_0) + init(BUTTON_AT_INT0); // 1. button +# else + init(BUTTON_AT_INT1_OR_PCINT); // 2. button +# endif +#endif +} +/* + * The same with aButtonPressCallback + * If both buttons are enabled, it initializes the 1. button (USE_BUTTON_0) + */ +EasyButton::EasyButton(void (*aButtonPressCallback)(bool aButtonToggleState)) { // @suppress("Class members should be properly initialized") + ButtonPressCallback = aButtonPressCallback; +#if !defined(NO_INITIALIZE_IN_CONSTRUCTOR) +# if defined(USE_BUTTON_0) + init(BUTTON_AT_INT0); // 1. button +# else + init(BUTTON_AT_INT1_OR_PCINT); // 2. button +# endif +#endif +} + +#if !defined(NO_BUTTON_RELEASE_CALLBACK) +EasyButton::EasyButton(void (*aButtonPressCallback)(bool aButtonToggleState), + void (*aButtonReleaseCallback)(bool aButtonToggleState, uint16_t aButtonPressDurationMillis)) { // @suppress("Class members should be properly initialized") + ButtonPressCallback = aButtonPressCallback; + ButtonReleaseCallback = aButtonReleaseCallback; +#if !defined(NO_INITIALIZE_IN_CONSTRUCTOR) +# if defined(USE_BUTTON_0) + init(BUTTON_AT_INT0); // 1. button +# else + init(BUTTON_AT_INT1_OR_PCINT); // 2. button +# endif +# endif +} +#endif // NO_BUTTON_RELEASE_CALLBACK + +/** + * @param aIsButtonAtINT0 true if this button is connected to INT0 i.e. is button 0 + */ +EasyButton::EasyButton(bool aIsButtonAtINT0) { +#if !(defined(USE_BUTTON_0) && defined(USE_BUTTON_1)) + (void) aIsButtonAtINT0; // to suppress compiler warnings +#endif +#if defined(USE_BUTTON_0) && not defined(USE_BUTTON_1) + init(BUTTON_AT_INT0); // 1. button +#elif defined(USE_BUTTON_1) && not defined(USE_BUTTON_0) + init(BUTTON_AT_INT1_OR_PCINT); // 2. button +#else + init(aIsButtonAtINT0); +#endif +} + +/** + * @param aIsButtonAtINT0 true if this button is connected to INT0 i.e. is button 0 + */ +EasyButton::EasyButton(bool aIsButtonAtINT0, void (*aButtonPressCallback)(bool aButtonToggleState)) { +#if !(defined(USE_BUTTON_0) && defined(USE_BUTTON_1)) + (void) aIsButtonAtINT0; // to suppress compiler warnings +#endif + ButtonPressCallback = aButtonPressCallback; +#if defined(USE_BUTTON_0) && not defined(USE_BUTTON_1) + init(BUTTON_AT_INT0); // 1. button +#elif defined(USE_BUTTON_1) && not defined(USE_BUTTON_0) + init(BUTTON_AT_INT1_OR_PCINT); // 2. button +#else + init(aIsButtonAtINT0); +#endif +} + +#if !defined(NO_BUTTON_RELEASE_CALLBACK) +/** + * @param aIsButtonAtINT0 true if this button is connected to INT0 i.e. is button 0 + */ +EasyButton::EasyButton(bool aIsButtonAtINT0, void (*aButtonPressCallback)(bool aButtonToggleState), + void (*aButtonReleaseCallback)(bool aButtonToggleState, uint16_t aButtonPressDurationMillis)) { +# if !(defined(USE_BUTTON_0) && defined(USE_BUTTON_1)) + (void) aIsButtonAtINT0; // to suppress compiler warnings +# endif + ButtonPressCallback = aButtonPressCallback; + ButtonReleaseCallback = aButtonReleaseCallback; +# if defined(USE_BUTTON_0) && not defined(USE_BUTTON_1) +init(BUTTON_AT_INT0); // 1. button +# elif defined(USE_BUTTON_1) && not defined(USE_BUTTON_0) +init(BUTTON_AT_INT1_OR_PCINT); // 2. button +# else + init(aIsButtonAtINT0); +# endif +} +#endif // NO_BUTTON_RELEASE_CALLBACK + +/* + * Sets pin mode to INPUT_PULLUP if not defined(BUTTON_IS_ACTIVE_HIGH) and enables INT0 Interrupt on any logical change. + * @param aIsButtonAtINT0 true if this button is connected to INT0 i.e. is button 0 + */ +void EasyButton::init(bool aIsButtonAtINT0) { + isButtonAtINT0 = aIsButtonAtINT0; +#if defined(MEASURE_EASY_BUTTON_INTERRUPT_TIMING) + pinModeFast(INTERRUPT_TIMING_OUTPUT_PIN, OUTPUT); +#endif + +#if defined(BUTTON_LED_FEEDBACK) + pinModeFast(BUTTON_LED_FEEDBACK_PIN, OUTPUT); +#endif + +#if defined(USE_BUTTON_0) && not defined(USE_BUTTON_1) + /* + * Only button 0 requested + */ + INT0_DDR_PORT &= ~(_BV(INT0_BIT)); // pinModeFast(2, INPUT_PULLUP); +# if !defined(BUTTON_IS_ACTIVE_HIGH) + INT0_OUT_PORT |= _BV(INT0_BIT); // enable pullup +# endif + sPointerToButton0ForISR = this; +# if defined(USE_ATTACH_INTERRUPT) + attachInterrupt(digitalPinToInterrupt(INT0_PIN), &handleINT0Interrupt, CHANGE); +# else + EICRA |= _BV(ISC00); // interrupt on any logical change + EIFR |= _BV(INTF0);// clear interrupt bit + EIMSK |= _BV(INT0);// enable interrupt on next change +# endif //USE_ATTACH_INTERRUPT + +#elif defined(USE_BUTTON_1) && not defined(USE_BUTTON_0) + /* + * Only button 1 requested + */ + INT1_DDR_PORT &= ~(_BV(INT1_BIT)); +# if !defined(BUTTON_IS_ACTIVE_HIGH) + INT1_OUT_PORT |= _BV(INT1_BIT); // enable pullup +# endif + sPointerToButton1ForISR = this; + +# if (!defined(ISC10)) || ((defined(__AVR_ATtiny87__) || defined(__AVR_ATtiny167__)) && INT1_PIN != 3) +# if defined(PCICR) + PCICR |= _BV(PCIE0); // Enable pin change interrupt for port PA0 to PA7 + PCMSK0 = digitalPinToBitMask(INT1_PIN); +# else + // ATtinyX5 no ISC10 flag existent + GIMSK |= _BV(PCIE); //PCINT enable, we have only one + PCMSK = digitalPinToBitMask(INT1_PIN); +# endif +# elif (INT1_PIN != 3) + /* + * ATmega328 (Uno, Nano ) etc. Enable pin change interrupt for port PD0 to PD7 (Arduino pin 0 to 7) + */ + PCICR |= _BV(PCIE2); + PCMSK2 = digitalPinToBitMask(INT1_PIN); +# else +# if defined(USE_ATTACH_INTERRUPT) + attachInterrupt(digitalPinToInterrupt(INT1_PIN), &handleINT1Interrupt, CHANGE); +# else + EICRA |= _BV(ISC10); // interrupt on any logical change + EIFR |= _BV(INTF1); // clear interrupt bit + EIMSK |= _BV(INT1); // enable interrupt on next change +# endif //USE_ATTACH_INTERRUPT +# endif // !defined(ISC10) + +#elif defined(USE_BUTTON_0) && defined(USE_BUTTON_1) + /* + * Both buttons 0 + 1 requested + */ + if (isButtonAtINT0) { + /* + * Button 0 + */ + INT0_DDR_PORT &= ~(_BV(INT0_BIT)); // pinModeFast(2, INPUT); +# if !defined(BUTTON_IS_ACTIVE_HIGH) + INT0_OUT_PORT |= _BV(INT0_BIT); // enable pullup +# endif + sPointerToButton0ForISR = this; +# if defined(USE_ATTACH_INTERRUPT) + attachInterrupt(digitalPinToInterrupt(INT0_PIN), &handleINT0Interrupt, CHANGE); +# else + EICRA |= _BV(ISC00); // interrupt on any logical change + EIFR |= _BV(INTF0); // clear interrupt bit + EIMSK |= _BV(INT0); // enable interrupt on next change +# endif //USE_ATTACH_INTERRUPT + } else { + /* + * Button 1 + * Allow PinChangeInterrupt + */ + INT1_DDR_PORT &= ~(_BV(INT1_BIT)); // pinModeFast(INT1_BIT, INPUT_PULLUP); +# if !defined(BUTTON_IS_ACTIVE_HIGH) + INT1_OUT_PORT |= _BV(INT1_BIT); // enable pullup +# endif + sPointerToButton1ForISR = this; + + /* + * Enable interrupt for 2. buttons + */ +# if (!defined(ISC10)) || ((defined(__AVR_ATtiny87__) || defined(__AVR_ATtiny167__)) && INT1_PIN != 3) +# if defined(PCICR) + /* + * ATtiny167 + 87. Enable pin change interrupt for port PA0 to PA7 + */ + PCICR |= _BV(PCIE0); + PCMSK0 = digitalPinToBitMask(INT1_PIN); +# else + /* + *ATtinyX5. Enable pin change interrupt for port PB0 to PB5 + */ + GIMSK |= _BV(PCIE); // PCINT enable, we have only one + PCMSK = digitalPinToBitMask(INT1_PIN); +# endif +# elif INT1_PIN == 4 || INT1_PIN == 5 || INT1_PIN == 6 || INT1_PIN == 7 + //ATmega328 (Uno, Nano ) etc. Enable pin change interrupt for port PD0 to PD7 (Arduino pin 0 to 7) + PCICR |= _BV(PCIE2); + PCMSK2 = digitalPinToBitMask(INT1_PIN); +# elif INT1_PIN == 8 || INT1_PIN == 9 || INT1_PIN == 10 || INT1_PIN == 11 || INT1_PIN == 12 || INT1_PIN == 13 + //ATmega328 (Uno, Nano ) etc. Enable pin change interrupt 0 to 5 for port PB0 to PB5 (Arduino pin 8 to 13) + PCICR |= _BV(PCIE0); + PCMSK0 = digitalPinToBitMask(INT1_PIN); +# elif INT1_PIN == A0 || INT1_PIN == A1 || INT1_PIN == A2 || INT1_PIN == A3 || INT1_PIN == A4 || INT1_PIN == A5 + //ATmega328 (Uno, Nano ) etc. Enable pin change interrupt 8 to 13 for port PC0 to PC5 (Arduino pin A0 to A5) + PCICR |= _BV(PCIE1); + PCMSK1 = digitalPinToBitMask(INT1_PIN); +# else +# if defined(USE_ATTACH_INTERRUPT) + attachInterrupt(digitalPinToInterrupt(INT1_PIN), &handleINT1Interrupt, CHANGE); +# else + // ATmega328 here + EICRA |= _BV(ISC10); // interrupt on any logical change + EIFR |= _BV(INTF1); // clear interrupt bit + EIMSK |= _BV(INT1); // enable interrupt on next change +# endif //USE_ATTACH_INTERRUPT +# endif // !defined(ISC10) + } +#endif + ButtonStateIsActive = false; // negative logic for ButtonStateIsActive! true means button pin is LOW + ButtonToggleState = false; +} + +/* + * Negative logic for readButtonState() true means button pin is LOW, if button is active low (default) + */ +bool EasyButton::readButtonState() { +#if defined(USE_BUTTON_0) && not defined(USE_BUTTON_1) +# if defined(BUTTON_IS_ACTIVE_HIGH) + return (INT0_IN_PORT & _BV(INT0_BIT)); // = digitalReadFast(2); +# else + return !(INT0_IN_PORT & _BV(INT0_BIT)); // = digitalReadFast(2); +# endif + +#elif defined(USE_BUTTON_1) && not defined(USE_BUTTON_0) +# if defined(BUTTON_IS_ACTIVE_HIGH) + return (INT1_IN_PORT & _BV(INT1_BIT)); // = digitalReadFast(3); +# else + return !(INT1_IN_PORT & _BV(INT1_BIT)); // = digitalReadFast(3); +# endif + +#elif defined(USE_BUTTON_0) && defined(USE_BUTTON_1) +# if defined(BUTTON_IS_ACTIVE_HIGH) + if (isButtonAtINT0) { + return (INT0_IN_PORT & _BV(INT0_BIT)); // = digitalReadFast(2); + } else { + return (INT1_IN_PORT & _BV(INT1_BIT)); // = digitalReadFast(3); + } +# else + if (isButtonAtINT0) { + return !(INT0_IN_PORT & _BV(INT0_BIT)); // = digitalReadFast(2); + } else { + return !(INT1_IN_PORT & _BV(INT1_BIT)); // = digitalReadFast(3); + } +# endif +#else + return false; +#endif +} + +// @formatter:on + +/* + * Returns stored state if in debouncing period otherwise current state of button + */ +bool EasyButton::readDebouncedButtonState() { + // Check for bouncing period + if (millis() - ButtonLastChangeMillis <= BUTTON_DEBOUNCING_MILLIS) { + return ButtonStateIsActive; + } + return readButtonState(); +} + +/* + * Update button state if state change was not captured by the ISR + * @return true if state was changed and updated + */ +bool EasyButton::updateButtonState() { + noInterrupts(); + if (readDebouncedButtonState() != ButtonStateIsActive) { +#if defined(TRACE) + if (LastBounceWasChangeToInactive) { + Serial.print(F("Updated button state, assume last button press was shorter than debouncing period of ")); + Serial.print(BUTTON_DEBOUNCING_MILLIS); + Serial.print(F(" ms")); +#if defined(ANALYZE_MAX_BOUNCING_PERIOD) + Serial.print(F(" MaxBouncingPeriod was=")); + Serial.print(MaxBouncingPeriodMillis); + MaxBouncingPeriodMillis = 0; +#endif + } else { + // It can happen, that we just catch the release of the button here, so no worry! + Serial.print(F("Update button state to ")); + Serial.print(!ButtonStateIsActive); + Serial.print(F(", current state was not yet caught by ISR")); + } + Serial.println(); +#endif + handleINT01Interrupts(); + interrupts(); + return true; + } + interrupts(); + return false; +} + +/* + * Updates the ButtonPressDurationMillis by polling, since this cannot be done by interrupt. + */ +uint16_t EasyButton::updateButtonPressDuration() { + if (readDebouncedButtonState()) { + // Button still active -> update ButtonPressDurationMillis + ButtonPressDurationMillis = millis() - ButtonLastChangeMillis; + } + return ButtonPressDurationMillis; +} + +/* + * Used for long button press recognition, while button is still pressed! + * !!! Consider to use button release callback handler and check the ButtonPressDurationMillis + * returns EASY_BUTTON_LONG_PRESS_DETECTED, EASY_BUTTON_LONG_PRESS_STILL_POSSIBLE and EASY_BUTTON_LONG_PRESS_ABORT + */ +uint8_t EasyButton::checkForLongPress(uint16_t aLongPressThresholdMillis) { + uint8_t tRetvale = EASY_BUTTON_LONG_PRESS_ABORT; + if (readDebouncedButtonState()) { + // Button still active -> update current ButtonPressDurationMillis + // noInterrupts() is required, since otherwise we may get wrong results if interrupted during load of long value by button ISR + noInterrupts(); + ButtonPressDurationMillis = millis() - ButtonLastChangeMillis; + interrupts(); + tRetvale = EASY_BUTTON_LONG_PRESS_STILL_POSSIBLE; // if not detected, you may try again + } + if (ButtonPressDurationMillis >= aLongPressThresholdMillis) { + // long press detected + return EASY_BUTTON_LONG_PRESS_DETECTED; + } + + return tRetvale; +} + +/* + * Checks for long press of button + * Blocks until long press threshold is reached or button was released. + * !!! Consider to use button release callback handler and check the ButtonPressDurationMillis + * @return true if long press was detected - only once for each long press + */ +bool EasyButton::checkForLongPressBlocking(uint16_t aLongPressThresholdMillis) { + /* + * wait as long as button is pressed shorter than threshold millis. + */ + while (checkForLongPress(aLongPressThresholdMillis) == EASY_BUTTON_LONG_PRESS_STILL_POSSIBLE) { + delay(1); + } + /* + * Here button was not pressed or time was greater than threshold. + * ButtonPressDurationMillis was updated by call to checkForLongPress before + */ + return (ButtonPressDurationMillis >= aLongPressThresholdMillis); +} + +/* + * Double press detection by computing difference between current (active) timestamp ButtonLastChangeMillis + * and last release timestamp ButtonReleaseMillis. + * !!!Works only reliable if called early in ButtonPress callback function!!! + * @return true if double press detected. + */ +bool EasyButton::checkForDoublePress(uint16_t aDoublePressDelayMillis) { + /* + * Check if ButtonReleaseMillis is not in initialized state + * otherwise a single press before aDoublePressDelayMillis after boot is mistakenly detected as double press + * Check costs 26 bytes program memory :-( + */ + if (ButtonReleaseMillis != 0) { + // because ButtonReleaseMillis is initialized with 0 milliseconds, which is interpreted as the first press happened at the beginning of boot. + unsigned long tReleaseToPressTimeMillis = ButtonLastChangeMillis - ButtonReleaseMillis; + return (tReleaseToPressTimeMillis <= aDoublePressDelayMillis); + } + return false; +} + +/* + * Checks if button was not pressed in the last aTimeoutMillis + * Can be used to recognize timeout for user button actions + * @return true if timeout reached: false if last button release was before aTimeoutMillis + */ +bool EasyButton::checkForForButtonNotPressedTime(uint16_t aTimeoutMillis) { + // noInterrupts() is required, since otherwise we may get wrong results if interrupted during load of long value by button ISR + noInterrupts(); + unsigned long tButtonReleaseMillis = ButtonReleaseMillis; + interrupts(); + return (millis() - tButtonReleaseMillis >= aTimeoutMillis); +} + +/* + * 1. Read button pin level and invert logic level since we have negative logic because of using pullups. + * 2. Check for bouncing - state change during debounce period. We need millis() to be enabled to run in the background. + * 3. Check for spikes - interrupts but no level change. + * 4. Process valid button state change. If callback requested, call callback routine, get button pin level again and handle if button was released in the meantime. + */ +void EasyButton::handleINT01Interrupts() { + // Read button value + bool tCurrentButtonStateIsActive; + + /* + * This is faster than readButtonState(); + */ +#if defined(USE_BUTTON_0) && not defined(USE_BUTTON_1) + tCurrentButtonStateIsActive = INT0_IN_PORT & _BV(INT0_BIT); // = digitalReadFast(2); + +#elif defined(USE_BUTTON_1) && not defined(USE_BUTTON_0) + tCurrentButtonStateIsActive = INT1_IN_PORT & _BV(INT1_BIT); // = digitalReadFast(3); + +#elif defined(USE_BUTTON_0) && defined(USE_BUTTON_1) + if (isButtonAtINT0) { + tCurrentButtonStateIsActive = INT0_IN_PORT & _BV(INT0_BIT); // = digitalReadFast(2); + } else { + tCurrentButtonStateIsActive = INT1_IN_PORT & _BV(INT1_BIT); // = digitalReadFast(3); + } +#endif +#if !defined(BUTTON_IS_ACTIVE_HIGH) + tCurrentButtonStateIsActive = !tCurrentButtonStateIsActive; // negative logic for tCurrentButtonStateIsActive! true means button pin is LOW +#endif + +#if defined(TRACE) + Serial.print(tCurrentButtonStateIsActive); + Serial.print('-'); +#endif + + unsigned long tMillis = millis(); + unsigned int tDeltaMillis = tMillis - ButtonLastChangeMillis; + // Check for bouncing - state change during debounce period + if (tDeltaMillis <= BUTTON_DEBOUNCING_MILLIS) { + /* + * Button is bouncing, signal is ringing - do nothing, ignore and wait for next interrupt + */ +#if defined(ANALYZE_MAX_BOUNCING_PERIOD) + if (MaxBouncingPeriodMillis < tDeltaMillis) { + MaxBouncingPeriodMillis = tDeltaMillis; + Serial.print(F("Bouncing, MBP=")); + Serial.println(MaxBouncingPeriodMillis); + // Serial.print(F("ms=")); + // Serial.print(tMillis); + // Serial.print(F(" D=")); + // Serial.println(tDeltaMillis); + } +#endif + if (tCurrentButtonStateIsActive) { + LastBounceWasChangeToInactive = false; + } else { + /* + * Store, that switch goes inactive during debouncing period. + * This may be a bouncing issue (fine) but it can also be a very short button press. + * In this case we do not set the ButtonStateIsActive to false because we are in debouncing period. + * On the next press this will be detected as a spike, if not considered. + */ + LastBounceWasChangeToInactive = true; + } + + } else { + /* + * Here we are after debouncing period + */ + if (tCurrentButtonStateIsActive == ButtonStateIsActive) { + /* + * No valid change detected - current is equals last state + */ + if (tCurrentButtonStateIsActive && LastBounceWasChangeToInactive) { + // We assume we had a very short press before (or a strange spike), which was handled as a bounce. -> must adjust last button state + ButtonStateIsActive = false; +#if defined(TRACE) + Serial.println(F("Preceding short press detected, which was handled as bounce")); +#endif + + } else { + /* + * tCurrentButtonStateIsActive == OldButtonStateIsActive. We had an interrupt, but nothing seems to have changed -> spike + * Do nothing, ignore and wait for next interrupt + */ +#if defined(TRACE) + Serial.println(F("Spike")); +#endif + } + } + + // do not use else since we may have changed ButtonStateIsActive + if (tCurrentButtonStateIsActive != ButtonStateIsActive) { + /* + * Valid change detected + */ + ButtonLastChangeMillis = tMillis; + LastBounceWasChangeToInactive = false; +#if defined(TRACE) + Serial.println(F("Change")); +#endif + ButtonStateIsActive = tCurrentButtonStateIsActive; + ButtonStateHasJustChanged = true; + if (tCurrentButtonStateIsActive) { + /* + * Button pressed + */ +#if defined(BUTTON_LED_FEEDBACK) + digitalWriteFast(BUTTON_LED_FEEDBACK_PIN, HIGH); +#endif + ButtonToggleState = !ButtonToggleState; + if (ButtonPressCallback != NULL) { + /* + * Call callback function. + * interrupts() is required if callback function needs more time to allow millis() to proceed. + * Otherwise we may see bouncing instead of button release followed by spike instead of button press + */ + interrupts(); + ButtonPressCallback(ButtonToggleState); + /* + * Check button again since it may changed back while processing callback function + */ + if (!readButtonState()) { + // button released now, so maintain status +#if defined(TRACE) + Serial.println(F("Button release during callback processing detected.")); +#endif + ButtonStateIsActive = false; + tMillis = millis(); + ButtonPressDurationMillis = tMillis - ButtonLastChangeMillis; + ButtonLastChangeMillis = tMillis; + ButtonStateHasJustChanged = true; + ButtonReleaseMillis = tMillis; + } + } + } else { + /* + * Button release + */ + ButtonPressDurationMillis = tDeltaMillis; + ButtonReleaseMillis = tMillis; +#if !defined(NO_BUTTON_RELEASE_CALLBACK) + if (ButtonReleaseCallback != NULL) { + /* + * Call callback function. + * interrupts() is required if callback function needs more time to allow millis() to proceed. + */ + interrupts(); + ButtonReleaseCallback(ButtonToggleState, ButtonPressDurationMillis); + /* + * Check button again since it may be activated while processing callback function + */ + if (readButtonState()) { + // button activated now, so maintain status +# if defined(TRACE) + Serial.println(F("Button active after callback processing detected.")); +# endif + ButtonStateIsActive = true; + ButtonLastChangeMillis = millis(); + ButtonStateHasJustChanged = true; + LastBounceWasChangeToInactive = false; + } + } +#endif +#if defined(BUTTON_LED_FEEDBACK) + digitalWriteFast(BUTTON_LED_FEEDBACK_PIN, LOW); +#endif + } + } + } +} + +// end of class definitions + +/* + * This functions are weak and can be replaced by your own code + */ +#if defined(USE_BUTTON_0) +void __attribute__ ((weak)) handleINT0Interrupt() { + EasyButton::sPointerToButton0ForISR->handleINT01Interrupts(); +} +#endif + +#if defined(USE_BUTTON_1) +void __attribute__ ((weak)) handleINT1Interrupt() { + EasyButton::sPointerToButton1ForISR->handleINT01Interrupts(); +} +#endif + +#if not defined(USE_ATTACH_INTERRUPT) +// 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(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 + +# if defined(USE_BUTTON_1) +# if (!defined(ISC10)) || ((defined(__AVR_ATtiny87__) || defined(__AVR_ATtiny167__)) && INT1_PIN != 3) +// on ATtinyX5 we do not have a INT1_vect but we can use the PCINT0_vect +ISR(PCINT0_vect) +# elif INT1_PIN == 4 || INT1_PIN == 5 || INT1_PIN == 6 || INT1_PIN == 7 +// PCINT for ATmega328 Arduino pins 0 to 7 +ISR(PCINT2_vect) +# elif INT1_PIN == 8 || INT1_PIN == 9 || INT1_PIN == 10 || INT1_PIN == 11 || INT1_PIN == 12 || INT1_PIN == 13 +// PCINT for ATmega328 Arduino pins 8 (PB0) to 13 (PB5) - (PCINT 0 to 5) +ISR(PCINT0_vect) +# elif INT1_PIN == A0 || INT1_PIN == A1 || INT1_PIN == A2 || INT1_PIN == A3 || INT1_PIN == A4 || INT1_PIN == A5 +// PCINT for ATmega328 Arduino pins A1 (PC0) to A5 (PC5) - (PCINT 8 to 13) +ISR(PCINT1_vect) +# else +ISR(INT1_vect) +# endif +{ +# if defined(MEASURE_EASY_BUTTON_INTERRUPT_TIMING) + digitalWriteFast(INTERRUPT_TIMING_OUTPUT_PIN, HIGH); +# endif + handleINT1Interrupt(); +# if defined(MEASURE_EASY_BUTTON_INTERRUPT_TIMING) + digitalWriteFast(INTERRUPT_TIMING_OUTPUT_PIN, LOW); +# endif +} +# endif +#endif // not defined(USE_ATTACH_INTERRUPT) + +#endif // defined(__AVR__) +#endif // _EASY_BUTTON_AT_INT01_HPP diff --git a/JK-BMSToPylontechCAN/JK-BMS.cpp b/JK-BMSToPylontechCAN/JK-BMS.cpp new file mode 100644 index 0000000..513793b --- /dev/null +++ b/JK-BMSToPylontechCAN/JK-BMS.cpp @@ -0,0 +1,661 @@ +/* + * JK-BMS.cpp + * + * Functions to read, convert and print JK-BMS data + * + * + * Copyright (C) 2023 Armin Joachimsmeyer + * Email: armin.joachimsmeyer@gmail.com + * + * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * + * Arduino-Utils 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 _JK_BMS_RS458_C +#define _JK_BMS_RS458_C + +#include + +#include "JK-BMS.h" + +#if !defined(LOCAL_DEBUG) +//#define LOCAL_DEBUG +#endif + +// see JK Communication protocol.pdf http://www.jk-bms.com/Upload/2022-05-19/1621104621.pdf +uint8_t JKRequestStatusFrame[21] = { 0x4E, 0x57 /*4E 57 = StartOfFrame*/, 0x00, 0x13 /*0x13 | 19 = LengthOfFrame*/, 0x00, 0x00, + 0x00, 0x00/*BMS ID, highest byte is default 00*/, 0x06/*Function 1=Activate, 3=ReadIdentifier, 6=ReadAllData*/, + 0x03/*Frame source 0=BMS, 1=Bluetooth, 2=GPRS, 3=PC*/, 0x00 /*TransportType 0=Request, 1=Response, 2=BMSActiveUpload*/, + 0x00/*0=ReadAllData or commandToken*/, 0x00, 0x00, 0x00, + 0x00/*RecordNumber High byte is random code, low 3 bytes is record number*/, JK_FRAME_END_BYTE/*0x68 = EndIdentifier*/, + 0x00, 0x00, 0x01, 0x29 /*Checksum, high 2 bytes for checksum not yet enabled -> 0, low 2 Byte for checksum*/}; +uint8_t JKrequestStatusFrameOld[] = { 0xDD, 0xA5, 0x03, 0x00, 0xFF, 0xFD, 0x77 }; + +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 +JKConvertedCellInfoStruct JKConvertedCellInfo; // The converted little endian cell voltage data +JKComputedDataStruct JKComputedData; // All derived converted and computed data useful for display +char sUpTimeString[16]; // " -> 1000D23H12M" is 15 bytes long +char sLastUpTimeCharacter; // for detecting changes in string +bool sForcePrintUpTime = true; // for LCD printing + +/* + * The JKFrameAllDataStruct starts behind the header + cell data header 0x79 + CellInfoSize + the variable length cell data (CellInfoSize is contained in JKReplyFrameBuffer[12]) + */ +JKReplyStruct *sJKFAllReplyPointer; + +const char lowCapacity[] PROGMEM = "Low capacity"; // Byte 0.0, warning +const char MosFetOvertemperature[] PROGMEM = "Power MosFet overtemperature"; // Byte 0.1; alarm +const char chargingOvervoltage[] PROGMEM = "Charging overvoltage"; // Byte 0.2, alarm +const char dischargingUndervoltage[] PROGMEM = "Discharging undervoltage"; // Byte 0.3, alarm +const char Sensor2Overtemperature[] PROGMEM = "Sensor1_2 overtemperature"; // Byte 0.4, alarm +const char chargingOvercurrent[] PROGMEM = "Charging overcurrent"; // Byte 0.5, alarm +const char dischargingOvercurrent[] PROGMEM = "Discharging overcurrent"; // Byte 0.6, alarm +const char CellVoltageDifference[] PROGMEM = "Cell voltage difference"; // Byte 0.7, alarm +const char Sensor1Overtemperature[] PROGMEM = "Sensor2 overtemperature"; // Byte 1.0, alarm +const char Sensor2LowLemperature[] PROGMEM = "Sensor1_2 low temperature"; // Byte 1.1, alarm +const char CellOvervoltage[] PROGMEM = "Cell overvoltage"; // Byte 1.2, alarm +const char CellUndervoltage[] PROGMEM = "Cell undervoltage"; // Byte 1.3, alarm +const char _309AProtection[] PROGMEM = "309_A protection"; // Byte 1.4, alarm +const char _309BProtection[] PROGMEM = "309_B protection"; // Byte 1.5, alarm + +const char *const JK_BMSErrorStringsArray[NUMBER_OF_DEFINED_ALARM_BITS] PROGMEM = { lowCapacity, MosFetOvertemperature, + chargingOvervoltage, dischargingUndervoltage, Sensor2Overtemperature, chargingOvercurrent, dischargingOvercurrent, + CellVoltageDifference, Sensor1Overtemperature, Sensor2LowLemperature, CellOvervoltage, CellUndervoltage, _309AProtection, + _309BProtection }; +const char *ErrorStringForLCD; // store of the error string of the highest error bit, NULL otherwise + +void requestJK_BMSStatusFrame(SoftwareSerialTX *aSerial, bool aDebugModeActive) { + for (uint8_t i = 0; i < sizeof(JKRequestStatusFrame); ++i) { + aSerial->write(JKRequestStatusFrame[i]); + } + if (aDebugModeActive) { + Serial.println(F("Send requestFrame with TxToJKBMS")); + for (uint8_t i = 0; i < sizeof(JKRequestStatusFrame); ++i) { + Serial.print(F(" 0x")); + Serial.print(JKRequestStatusFrame[i], HEX); + } + Serial.println(); + } +} + +void initJKReplyFrameBuffer() { + sReplyFrameBufferIndex = 0; +} + +void printJKReplyFrameBuffer() { + if (sReplyFrameBufferIndex == 0) { + Serial.println(F("sReplyFrameBufferIndex is 0")); + } else { + for (uint16_t i = 0; i < (sReplyFrameBufferIndex + 1); ++i) { + if (i == JK_BMS_FRAME_HEADER_LENGTH || i == ((sReplyFrameBufferIndex + 1) - JK_BMS_FRAME_TRAILER_LENGTH) || i % 16 == 0 + || i + == (uint16_t) (JK_BMS_FRAME_INDEX_OF_CELL_INFO_LENGTH + 1 + + JKReplyFrameBuffer[JK_BMS_FRAME_INDEX_OF_CELL_INFO_LENGTH])) { + // Insert newline and address after header, after cell data and after each 16 bit + if (i != 0) { + Serial.println(); + } + Serial.print(F("0x")); + if (i < 0x10) { + Serial.print('0'); // padding with zero + } + Serial.print(i, HEX); + Serial.print(F(" ")); + } + + Serial.print(F("0x")); + if (JKReplyFrameBuffer[i] < 0x10) { + Serial.print('0'); // padding with zero + } + Serial.print(JKReplyFrameBuffer[i], HEX); + Serial.print(' '); + + } + Serial.println(); + } +} + +#define JK_BMS_RECEIVE_OK 0 +#define JK_BMS_RECEIVE_FINISHED 1 +#define JK_BMS_RECEIVE_ERROR 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_ERROR, if frame has errors. + */ +uint8_t readJK_BMSStatusFrameByte() { + uint8_t tReceivedByte = Serial.read(); + JKReplyFrameBuffer[sReplyFrameBufferIndex] = tReceivedByte; + + /* + * Plausi check and get length of frame + */ + if (sReplyFrameBufferIndex == 0) { + // start byte 1 + if (tReceivedByte != JK_FRAME_START_BYTE_0) { + Serial.println(F("Error start frame token != 0x4E")); + return JK_BMS_RECEIVE_ERROR; + } + } else if (sReplyFrameBufferIndex == 1) { + if (tReceivedByte != JK_FRAME_START_BYTE_1) { + // Error + return JK_BMS_RECEIVE_ERROR; + } + + } else if (sReplyFrameBufferIndex == 3) { + // length of frame + sReplyFrameLength = (JKReplyFrameBuffer[2] << 8) + tReceivedByte; + + } else if (sReplyFrameLength > MINIMAL_JK_BMS_FRAME_LENGTH && sReplyFrameBufferIndex == sReplyFrameLength - 3) { + // Check end token 0x68 + if (tReceivedByte != JK_FRAME_END_BYTE) { + Serial.print(F("Error 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(F(" | 0x")); + Serial.println(sReplyFrameLength, HEX); + return JK_BMS_RECEIVE_ERROR; + } + + } else if (sReplyFrameLength > MINIMAL_JK_BMS_FRAME_LENGTH && sReplyFrameBufferIndex == sReplyFrameLength + 1) { + /* + * Frame received completely, perform checksum check + */ + uint16_t tComputedChecksum = 0; + for (uint16_t i = 0; i < sReplyFrameLength - 2; i++) { + tComputedChecksum = tComputedChecksum + JKReplyFrameBuffer[i]; + } + uint16_t tReceivedChecksum = (JKReplyFrameBuffer[sReplyFrameLength] << 8) + tReceivedByte; + if (tComputedChecksum != tReceivedChecksum) { + Serial.print(F("Checksum error, computed checksum=0x")); + Serial.print(tComputedChecksum, HEX); + Serial.print(F(", received checksum=0x")); + Serial.println(tReceivedChecksum, HEX); + + return JK_BMS_RECEIVE_ERROR; + } else { + return JK_BMS_RECEIVE_FINISHED; + } + } + sReplyFrameBufferIndex++; + return JK_BMS_RECEIVE_OK; +} + +/* + * Charge is positive, discharge is negative + */ +int16_t getCurrent(uint16_t aJKRAWCurrent) { + uint16_t tCurrent = swap(aJKRAWCurrent); + if (tCurrent == 0 || (tCurrent & 0x8000) == 0x8000) { + // Charge + return (tCurrent & 0x7FFF); + } + // discharge + return tCurrent * -1; + +} + +int16_t getTemperature(uint16_t aJKRAWTemperature) { + uint16_t tTemperature = swap(aJKRAWTemperature); + if (tTemperature <= 100) { + return tTemperature; + } + return 100 - tTemperature; +} + +// Identity function to avoid swapping if accidentally called +uint8_t swap(uint8_t aByte) { + return (aByte); +} + +uint16_t swap(uint16_t aWordToSwapBytes) { + return ((aWordToSwapBytes << 8) | (aWordToSwapBytes >> 8)); +} + +uint32_t swap(uint32_t aLongToSwapBytes) { + return ((aLongToSwapBytes << 24) | ((aLongToSwapBytes & 0xFF00) << 8) | ((aLongToSwapBytes >> 8) & 0xFF00) + | (aLongToSwapBytes >> 24)); +} + +void myPrintln(const __FlashStringHelper *aPGMString, uint16_t a16BitValue) { + Serial.print(aPGMString); + Serial.println(a16BitValue); +} + +void myPrint(const __FlashStringHelper *aPGMString, uint16_t a16BitValue) { + Serial.print(aPGMString); + Serial.print(a16BitValue); +} + +void myPrintlnSwap(const __FlashStringHelper *aPGMString, uint16_t a16BitValue) { + Serial.print(aPGMString); + Serial.println(swap(a16BitValue)); +} + +void myPrintlnSwap(const __FlashStringHelper *aPGMString, int16_t a16BitValue) { + Serial.print(aPGMString); + Serial.println((int16_t) swap((uint16_t) a16BitValue)); +} + +void myPrintSwap(const __FlashStringHelper *aPGMString, int16_t a16BitValue) { + Serial.print(aPGMString); + Serial.print((int16_t) swap((uint16_t) a16BitValue)); +} + +void myPrintlnSwap(const __FlashStringHelper *aPGMString, uint32_t a32BitValue) { + Serial.print(aPGMString); + 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 + */ +void fillJKConvertedCellInfo() { +// uint8_t *tJKCellInfoReplyPointer = &TestJKReplyStatusFrame[11]; + uint8_t *tJKCellInfoReplyPointer = &JKReplyFrameBuffer[JK_BMS_FRAME_INDEX_OF_CELL_INFO_LENGTH]; + + uint8_t tNumberOfCellInfo = (*tJKCellInfoReplyPointer++) / 3; + JKConvertedCellInfo.NumberOfCellInfoEnties = tNumberOfCellInfo; + + uint16_t tVoltage; + uint32_t tMillivoltSum = 0; + uint8_t tNumberOfNonNullCellInfo = 0; + uint16_t tMinimumMillivolt = 0xFFFF; + uint16_t tMaximumMillivolt = 0; + + for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { + JKConvertedCellInfo.CellInfoStructArray[i].CellNumber = *tJKCellInfoReplyPointer++; // Copy CellNumber + + uint8_t tHighByte = *tJKCellInfoReplyPointer++; // Copy CellMillivolt + tVoltage = tHighByte << 8 | *tJKCellInfoReplyPointer++; + JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt = tVoltage; + if (tVoltage > 0) { + tNumberOfNonNullCellInfo++; + tMillivoltSum += tVoltage; + if (tMinimumMillivolt > tVoltage) { + tMinimumMillivolt = tVoltage; + JKConvertedCellInfo.MinimumVoltagCellIndex = i; + } + if (tMaximumMillivolt < tVoltage) { + tMaximumMillivolt = tVoltage; + JKConvertedCellInfo.MaximumVoltagCellIndex = i; + } + } + } + JKConvertedCellInfo.DeltaCellMillivolt = tMaximumMillivolt - tMinimumMillivolt; + JKConvertedCellInfo.AverageCellMillivolt = tMillivoltSum / tNumberOfNonNullCellInfo; + +#if defined(LOCAL_DEBUG) + Serial.print(tNumberOfCellInfo); + Serial.println(F(" cell voltages processed")); +#endif + if (tNumberOfNonNullCellInfo < tNumberOfCellInfo) { + Serial.print(F("Problem: ")); + Serial.print(tNumberOfCellInfo); + Serial.print(F(" cells configured, but only ")); + Serial.print(tNumberOfNonNullCellInfo); + Serial.println(F(" cells are connected")); + } +} + +void fillJKComputedData() { + JKComputedData.TemperaturePowerMosFet = getTemperature(sJKFAllReplyPointer->TemperaturePowerMosFet); + int16_t tMaxTemperature = JKComputedData.TemperaturePowerMosFet; + + JKComputedData.TemperatureSensor1 = getTemperature(sJKFAllReplyPointer->TemperatureSensor1); + if (tMaxTemperature < JKComputedData.TemperatureSensor1) { + tMaxTemperature = JKComputedData.TemperatureSensor1; + } + + JKComputedData.TemperatureSensor2 = getTemperature(sJKFAllReplyPointer->TemperatureSensor2); + if (tMaxTemperature < JKComputedData.TemperatureSensor2) { + tMaxTemperature = JKComputedData.TemperatureSensor2; + } + JKComputedData.TemperatureMaximum = tMaxTemperature; + + JKComputedData.TotalCapacityAmpereHour = swap(sJKFAllReplyPointer->TotalCapacityAmpereHour); + // 16 bit multiplication gives overflow at 640 Ah + JKComputedData.RemainingCapacityAmpereHour = ((uint32_t) JKComputedData.TotalCapacityAmpereHour + * sJKFAllReplyPointer->SOCPercent) / 100; + + JKComputedData.BatteryVoltage10Millivolt = swap(sJKFAllReplyPointer->Battery10Millivolt); + JKComputedData.BatteryVoltageFloat = JKComputedData.BatteryVoltage10Millivolt; + JKComputedData.BatteryVoltageFloat /= 100; + + JKComputedData.Battery10MilliAmpere = getCurrent(sJKFAllReplyPointer->Battery10MilliAmpere); + JKComputedData.BatteryLoadCurrentFloat = JKComputedData.Battery10MilliAmpere; + JKComputedData.BatteryLoadCurrentFloat /= 100; + +// Serial.print("Battery10MilliAmpere=0x"); +// Serial.print(sJKFAllReplyPointer->Battery10MilliAmpere, HEX); +// Serial.print(" Battery10MilliAmpere swapped=0x"); +// Serial.println(swap(sJKFAllReplyPointer->Battery10MilliAmpere), HEX); +// Serial.print(" Battery10MilliAmpere="); +// Serial.print(JKComputedData.Battery10MilliAmpere); +// Serial.print(" BatteryLoadCurrent="); +// Serial.println(JKComputedData.BatteryLoadCurrentFloat); + + JKComputedData.BatteryLoadPower = JKComputedData.BatteryVoltageFloat * JKComputedData.BatteryLoadCurrentFloat; +} + +void printJKCellInfo() { + uint8_t tNumberOfCellInfo = JKConvertedCellInfo.NumberOfCellInfoEnties; + +// Serial.print(tNumberOfCellInfo); +// Serial.println(F(" cell voltages:")); + for (uint8_t i = 0; i < tNumberOfCellInfo; ++i) { + if (i != 0 && (i % 8) == 0) { + Serial.println(); + } + if (JKConvertedCellInfo.CellInfoStructArray[i].CellNumber < 10) { + Serial.print(' '); + } + Serial.print(JKConvertedCellInfo.CellInfoStructArray[i].CellNumber); + Serial.print(F("=")); + Serial.print(JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt); +#if defined(LOCAL_TRACE) + Serial.print(F("|0x")); + Serial.print(JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt, HEX); +#endif + Serial.print(F(" mV, ")); + } + Serial.println(); + + /* + * Cell statistics + */ + myPrint(F("Minimum="), JKConvertedCellInfo.CellInfoStructArray[JKConvertedCellInfo.MinimumVoltagCellIndex].CellMillivolt); + myPrint(F(" mV at cell #"), JKConvertedCellInfo.MinimumVoltagCellIndex + 1); + myPrint(F(", Maximum="), JKConvertedCellInfo.CellInfoStructArray[JKConvertedCellInfo.MaximumVoltagCellIndex].CellMillivolt); + myPrintln(F(" mV at cell #"), JKConvertedCellInfo.MaximumVoltagCellIndex + 1); + myPrint(F("Delta="), JKConvertedCellInfo.DeltaCellMillivolt); + myPrint(F(" mV, Average="), JKConvertedCellInfo.AverageCellMillivolt); + Serial.println(F(" mV")); + Serial.println(); +} + +void printVoltageProtectionInfo() { + JKReplyStruct *tJKFAllReply = sJKFAllReplyPointer; + /* + * Voltage protection + */ + myPrint(F("Battery Overvoltage Protection[mV]="), swap(tJKFAllReply->BatteryOvervoltageProtection10Millivolt) * 10); + myPrintln(F(", Undervoltage="), swap(tJKFAllReply->BatteryUndervoltageProtection10Millivolt) * 10); + myPrintSwap(F("Cell Overvoltage Protection[mV]="), tJKFAllReply->CellOvervoltageProtectionMillivolt); + myPrintSwap(F(", Recovery="), tJKFAllReply->CellOvervoltageRecoveryMillivolt); + myPrintlnSwap(F(", Delay[s]="), tJKFAllReply->CellOvervoltageDelaySeconds); + myPrintSwap(F("Cell Undervoltage Protection[mV]="), tJKFAllReply->CellUndervoltageProtectionMillivolt); + myPrintSwap(F(", Recovery="), tJKFAllReply->CellUndervoltageRecoveryMillivolt); + myPrintlnSwap(F(", Delay[s]="), tJKFAllReply->CellUndervoltageDelaySeconds); + myPrintlnSwap(F("Cell Voltage Difference Protection[mV]="), tJKFAllReply->VoltageDifferenceProtectionMillivolt); + myPrintSwap(F("Discharging Overcurrent Protection[A]="), tJKFAllReply->DischargeOvercurrentProtectionAmpere); + myPrintlnSwap(F(", Delay[s]="), tJKFAllReply->DischargeOvercurrentDelaySeconds); + myPrintSwap(F("Charging Overcurrent Protection[A]="), tJKFAllReply->ChargeOvercurrentProtectionAmpere); + myPrintlnSwap(F(", Delay[s]="), tJKFAllReply->ChargeOvercurrentDelaySeconds); + Serial.println(); +} + +void printTemperatureProtectionInfo() { + JKReplyStruct *tJKFAllReply = sJKFAllReplyPointer; + /* + * Temperature protection + */ + myPrintSwap(F("Power MosFet Temperature Protection="), tJKFAllReply->PowerMosFetTemperatureProtection); + myPrintlnSwap(F(", Recovery="), tJKFAllReply->PowerMosFetRecoveryTemperature); + myPrintSwap(F("Sensor1 Temperature Protection="), tJKFAllReply->Sensor1TemperatureProtection); + myPrintlnSwap(F(", Recovery="), tJKFAllReply->Sensor1RecoveryTemperature); + myPrintlnSwap(F("Sensor1 to Sensor2 Temperature Difference Protection="), tJKFAllReply->BatteryDifferenceTemperatureProtection); + myPrintSwap(F("Charge Overtemperature Protection="), tJKFAllReply->ChargeOvertemperatureProtection); + myPrintlnSwap(F(", Discharge="), tJKFAllReply->DischargeOvertemperatureProtection); + myPrintSwap(F("Charge Undertemperature Protection="), tJKFAllReply->ChargeUndertemperatureProtection); + myPrintlnSwap(F(", Recovery="), tJKFAllReply->ChargeRecoveryUndertemperature); + myPrintSwap(F("Discharge Undertemperature Protection="), tJKFAllReply->DischargeUndertemperatureProtection); + myPrintlnSwap(F(", Recovery="), tJKFAllReply->DischargeRecoveryUndertemperature); + Serial.println(); +} + +void printBatteryInfo() { + JKReplyStruct *tJKFAllReply = sJKFAllReplyPointer; + + Serial.print(F("Manufacturer Id=")); + tJKFAllReply->TokenProtocolVersionNumber = '\0'; // set end of string token + Serial.println(tJKFAllReply->ManufacturerId); + Serial.print(F("Manufacturer Date=")); + tJKFAllReply->TokenSystemWorkingMinutes = '\0'; // set end of string token + Serial.println(tJKFAllReply->ManufacturerDate); + Serial.print(F("Device ID String=")); + tJKFAllReply->TokenManufacturerDate = '\0'; // set end of string token + Serial.println(tJKFAllReply->DeviceIdString); + myPrintln(F("Device Address="), tJKFAllReply->BoardAddress); + myPrint(F("Total Battery Capacity[Ah]="), JKComputedData.TotalCapacityAmpereHour); // 0xAA + myPrintln(F(", Low Capacity Alarm Percent="), tJKFAllReply->LowCapacityAlarmPercent); // 0xB1 + myPrintlnSwap(F("Charging Cycles="), tJKFAllReply->Cycles); + myPrintlnSwap(F("Total Charging Cycle Capacity="), tJKFAllReply->TotalBatteryCycleCapacity); + myPrintSwap(F("# Battery Cells="), tJKFAllReply->NumberOfBatteryCells); // 0x8A Total number of battery strings + myPrintln(F(", Cell Count="), tJKFAllReply->BatteryCellCount); // 0xA9 Battery string count settings + Serial.println(); +} + +void printBMSInfo() { + JKReplyStruct *tJKFAllReply = sJKFAllReplyPointer; + + myPrintln(F("Protocol Version Number="), tJKFAllReply->ProtocolVersionNumber); + Serial.print(F("Software Version Number=")); + tJKFAllReply->TokenStartCurrentCalibration = '\0'; // set end of string token + Serial.println(tJKFAllReply->SoftwareVersionNumber); + Serial.print(F("Modify Parameter Password=")); + tJKFAllReply->TokenDedicatedChargerSwitchState = '\0'; // set end of string token + Serial.println(tJKFAllReply->ModifyParameterPassword); + + myPrintln(F("# External Temperature Sensors="), tJKFAllReply->NumberOfTemperatureSensors); // 0x86 + + Serial.println(); +} + +void printMiscelaneousInfo() { + JKReplyStruct *tJKFAllReply = sJKFAllReplyPointer; + + myPrintlnSwap(F("Balance Starting Cell Voltage=[mV]"), tJKFAllReply->BalancingStartMillivolt); + myPrintlnSwap(F("Balance Opening Voltage Difference[mV]="), tJKFAllReply->BalancingStartDifferentialMillivolt); // ??? semantics + Serial.println(); + myPrintlnSwap(F("Current Calibration[mA]="), tJKFAllReply->CurrentCalibrationMilliampere); + myPrintlnSwap(F("Sleep Wait Time[s]="), tJKFAllReply->SleepWaitingTimeSeconds); + Serial.println(); + myPrintln(F("Dedicated Charge Switch Active="), tJKFAllReply->DedicatedChargerSwitchIsActive); + myPrintln(F("Start Current Calibration State="), tJKFAllReply->StartCurrentCalibration); + myPrintlnSwap(F("Battery Actual Capacity[Ah]="), tJKFAllReply->ActualBatteryCapacityAmpereHour); + Serial.println(); +} + +/* + * Token 0x8B. Prints info only if errors existent and changed from last value + * Stores error string for LCD in ErrorStringForLCD + */ +void printAlarmInfo() { + static uint16_t sLastAlarms = 0; + JKReplyStruct *tJKFAllReply = sJKFAllReplyPointer; + + uint16_t tAlarms = swap(tJKFAllReply->AlarmUnion.AlarmsAsWord); + + if (sLastAlarms != tAlarms) { + sLastAlarms = tAlarms; + if (tAlarms == 0) { + ErrorStringForLCD = NULL; + sForcePrintUpTime = true; // to force overwriting of alarm + } + } + + if (tAlarms != 0) { +// sLastAlarms = tAlarms; + Serial.println(F("*** ALARM FLAGS ***")); + + Serial.print(F("Alarm bits=0b")); + Serial.println(tAlarms, BIN); + + uint16_t tAlarmMask = 1; + for (uint_fast8_t i = 0; i < NUMBER_OF_DEFINED_ALARM_BITS; ++i) { + if (tAlarms & tAlarmMask) { + Serial.print(F("Alarm bit=0b")); + Serial.print(tAlarmMask, BIN); + Serial.print(F(" -> ")); + const char *tErrorStringPtr = (char*) (pgm_read_word(&JK_BMSErrorStringsArray[i])); + ErrorStringForLCD = tErrorStringPtr; + Serial.println(reinterpret_cast(tErrorStringPtr)); + } + tAlarmMask <<= 1; + } + } + Serial.println(); + +} + +void printEnabledState(bool aIsEnabled) { + if (aIsEnabled) { + Serial.print(F(" enabled")); + } else { + Serial.print(F(" disabled")); + } +} + +void printActiveState(bool aIsActive) { + if (!aIsActive) { + Serial.print(F(" not")); + } + Serial.print(F(" active")); +} +/* + * Token 0x8C. Print if changed + * NOT YET USED + */ +void printStatusFlags() { + static uint16_t sLastStatus = 0; + + JKReplyStruct *tJKFAllReply = sJKFAllReplyPointer; +// Serial.print(F("StatusAsWord=")); +// Serial.println(tJKFAllReply->StatusUnion.StatusAsWord); + + uint16_t tStatus = swap(tJKFAllReply->StatusUnion.StatusAsWord); + if (sLastStatus != tStatus) { + sLastStatus = tStatus; + Serial.println(F("*** STATUS FLAGS ***")); + Serial.print(F("Charging MosFet")); + printActiveState(tJKFAllReply->StatusUnion.StatusBits.ChargeMosFetActive); + Serial.print(F(", Discharging MosFet")); + printActiveState(tJKFAllReply->StatusUnion.StatusBits.DischargeMosFetActive); + Serial.print(F(", Balancer")); + printActiveState(tJKFAllReply->StatusUnion.StatusBits.BalancerActive); + Serial.print(F(", Shutdown")); + printActiveState(tJKFAllReply->StatusUnion.StatusBits.BatteryDown); + Serial.println(); + Serial.println(); + } +} + +void printJKStaticInfo() { + + Serial.println(F("*** BMS INFO ***")); + printBMSInfo(); + + Serial.println(F("*** BATTERY INFO ***")); + printBatteryInfo(); + + Serial.println(F("*** VOLTAGE PROTECTION INFO ***")); + printVoltageProtectionInfo(); + + Serial.println(F("*** TEMPERATURE PROTECTION INFO ***")); + printTemperatureProtectionInfo(); + + Serial.println(F("*** MISC INFO ***")); + printMiscelaneousInfo(); +} + +/* + * Print received data + * Use converted cell voltage info from JKConvertedCellInfo + * All other data are used unconverted and are therefore printed by swap() functions. + */ +void printJKDynamicInfo() { + + JKReplyStruct *tJKFAllReply = sJKFAllReplyPointer; + + Serial.println(F("*** CELL INFO ***")); + printJKCellInfo(); + + Serial.println(F("*** DYNAMIC INFO ***")); + uint32_t tSystemWorkingMinutes = swap(tJKFAllReply->SystemWorkingMinutes); + // 1 kByte for sprintf + sprintf_P(sUpTimeString, PSTR(" -> %4uD%2uH%2uM"), (uint16_t) (tSystemWorkingMinutes / (60 * 24)), + (uint16_t) ((tSystemWorkingMinutes / 60) % 24), (uint16_t) tSystemWorkingMinutes % 60); + if (sLastUpTimeCharacter != sUpTimeString[13]) { + sLastUpTimeCharacter = sUpTimeString[13]; + sForcePrintUpTime = true; + myPrint(F("Total Runtime Minutes="), tSystemWorkingMinutes); + Serial.println(sUpTimeString); + } + + /* + * Temperatures + */ +#if defined(LOCAL_DEBUG) + Serial.print(F("TokenTemperaturePowerMosFet=0x")); + Serial.println(tJKFAllReply->TokenTemperaturePowerMosFet, HEX); +#endif + myPrint(F("Temperature: Power MosFet="), JKComputedData.TemperaturePowerMosFet); + myPrint(F(", Sensor 1="), JKComputedData.TemperatureSensor1); + myPrintln(F(", Sensor 2="), JKComputedData.TemperatureSensor2); + Serial.println(); + + /* + * Capacity + */ + myPrint(F("SOC[%]="), tJKFAllReply->SOCPercent); + myPrintln(F(" -> Remaining Capacity[Ah]="), JKComputedData.RemainingCapacityAmpereHour); + + Serial.print(F("Battery Voltage[V]=")); + Serial.print(JKComputedData.BatteryVoltageFloat, 2); + Serial.print(F(", Current[A]=")); + Serial.print(JKComputedData.BatteryLoadCurrentFloat, 2); + myPrintln(F(", Power[W]="), JKComputedData.BatteryLoadPower); + Serial.println(); + + if (sJKFAllReplyPointer->StatusUnion.StatusBits.ChargeMosFetActive) { + + } + Serial.print(F("Charging MosFet")); + printEnabledState(tJKFAllReply->ChargeIsEnabled); + Serial.print(','); + printActiveState(tJKFAllReply->StatusUnion.StatusBits.ChargeMosFetActive); + Serial.print(F(" | Discharging MosFet")); + printEnabledState(tJKFAllReply->DischargeIsEnabled); + Serial.print(','); + printActiveState(tJKFAllReply->StatusUnion.StatusBits.DischargeMosFetActive); + Serial.print(F(" | Balancing")); + printEnabledState(tJKFAllReply->BalancingIsEnabled); + Serial.print(','); + printActiveState(tJKFAllReply->StatusUnion.StatusBits.BalancerActive); + + Serial.println(); + + printAlarmInfo(); + +} +#endif // _JK_BMS_H diff --git a/JK-BMSToPylontechCAN/JK-BMS.h b/JK-BMSToPylontechCAN/JK-BMS.h new file mode 100644 index 0000000..6cb9a2c --- /dev/null +++ b/JK-BMSToPylontechCAN/JK-BMS.h @@ -0,0 +1,340 @@ +/* + * JK-BMS.h + * + * Definitions of the data structures used by JK-BMS and the converter + * + * Copyright (C) 2023 Armin Joachimsmeyer + * Email: armin.joachimsmeyer@gmail.com + * + * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * + * Arduino-Utils 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 _JK_BMS_H +#define _JK_BMS_H + +#include +#include "SoftwareSerialTX.h" + +#if !defined(MAXIMUM_NUMBER_OF_CELLS) +#define MAXIMUM_NUMBER_OF_CELLS 24 // must be before #include "JK-BMS.h" +#endif +#define JK_FRAME_START_BYTE_0 0x4E +#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 fillJKConvertedCellInfo(); +void fillJKComputedData(); + +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 struct JKConvertedCellInfoStruct JKConvertedCellInfo; // The converted little endian cell voltage data +extern struct JKComputedDataStruct JKComputedData; // All derived converted and computed data useful for display +extern const char *ErrorStringForLCD; +extern char sUpTimeString[16]; // " -> 1000D23H12M" is 15 bytes long +extern bool sForcePrintUpTime; // for LCD printing + +int16_t getTemperature(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); +void printJKStaticInfo(); +void printJKDynamicInfo(); + +#define JK_BMS_FRAME_HEADER_LENGTH 11 +#define JK_BMS_FRAME_TRAILER_LENGTH 9 +#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 + +/* + * All 16 and 32 bit values are stored byte swapped, i.e. MSB is stored in lower address. + * Must be read with swap() + */ +struct JKFrameHeaderStruct { + uint16_t StartFrameToken; // 0x4E57 + uint16_t LengthOfFrame; // Excluding StartFrameToken + uint32_t BMS_ID; // Highest byte is default 00 + uint8_t Function; // 0x01 (activation), 0x02 (write), 0x03 (read), 0x05 (password), 0x06 (read all) + uint8_t FrameSource; // 0=BMS, 1=Bluetooth, 2=GPRS, 3=PC + uint8_t TransportType; // 0=Request, 1=Response, 2=BMSActiveUpload +}; + +struct JKFrameTailStruct { + uint32_t RecordNumber; // High byte is random code, low 3 bytes is record number + uint8_t EndToken; // 0x68 + uint16_t UnusedChecksum; // 0x0000 + uint16_t Checksum; // Including StartFrameToken +}; + +struct JKCellInfoStruct { + uint8_t CellNumber; + uint16_t CellMillivolt; +}; + +struct JKConvertedCellInfoStruct { + uint8_t NumberOfCellInfoEnties; + JKCellInfoStruct CellInfoStructArray[MAXIMUM_NUMBER_OF_CELLS]; + uint8_t MinimumVoltagCellIndex; + uint8_t MaximumVoltagCellIndex; + uint16_t DeltaCellMillivolt; // Difference between MinimumVoltagCell and MaximumVoltagCell + uint16_t AverageCellMillivolt; +}; + +/* + * This structure contains all converted and computed data useful for display + */ +struct JKComputedDataStruct { + int16_t TemperaturePowerMosFet; //degree Celsius + int16_t TemperatureSensor1; + int16_t TemperatureSensor2; + int16_t TemperatureMaximum; // Computed value + + uint16_t TotalCapacityAmpereHour; + uint16_t RemainingCapacityAmpereHour; // Computed value + uint16_t BatteryVoltage10Millivolt; + float BatteryVoltageFloat; // Volt + int16_t Battery10MilliAmpere; // Charging is positive discharging is negative + float BatteryLoadCurrentFloat; // Ampere + int16_t BatteryLoadPower; // Watt Computed value, Charging is positive discharging is negative +}; + +/* + * Structure representing the semantic of the JK reply, except cell voltage info. + * + * All 16 and 32 bit values in this structure are filled with big endian by the JK protocol i.e. the higher byte is located at the lower memory address. + * AVR and others are using little endian i.e. the lower byte is located at the lower memory address. + * !!! => all 16 and 32 bit values in this structure must be "swapped" before interpreting them!!! + * + * All temperatures are in degree celsius. + * Power MosFet temperature sensor is originally named PowerTube + * Sensor1 temperature sensor is originally named Battery Box + * Sensor2 temperature sensor is originally named Battery + * Battery values are often originally named Total + * + */ +#define NUMBER_OF_DEFINED_ALARM_BITS 14 +struct JKReplyStruct { + uint8_t TokenTemperaturePowerMosFet; // 0x80 + uint16_t TemperaturePowerMosFet; // 99 = 99 degree Celsius, 100 = 100, 101 = -1, 140 = -40 + uint8_t TokenTemperatureSensor1; // 0x81 TemperatureSensor1 + uint16_t TemperatureSensor1; // originally named Battery Box, the outmost sensor beneath the led + uint8_t TokenTemperatureSensor2; // 0x82 + uint16_t TemperatureSensor2; // originally named Battery, the inner sensor beneath the battery+ + uint8_t TokenVoltage; // 0x83 + uint16_t Battery10Millivolt; // Voltage values start with 200mA at 240 mA real current -> 410 -> 620 -> 830mA@920mA real -> 1030@1080mA real -> 1.33 => Resolution is 0.21A + uint8_t TokenCurrent; // 0x84 + uint16_t Battery10MilliAmpere; // Highest bit: 0=Discharge, 1=Charge -> see 0xC0 + uint8_t TokenSOCPercent; // 0x85 + uint8_t SOCPercent; // 0-100% + uint8_t TokenNumberOfTemperatureSensors; // 0x86 + uint8_t NumberOfTemperatureSensors; // 2 + uint8_t TokenCycles; // 0x87 + uint16_t Cycles; + uint8_t TokenTotalBatteryCycleCapacity; // 0x89 + uint32_t TotalBatteryCycleCapacity; // Ah + + uint8_t TokenNumberOfBatteryCells; // 0x8A + uint16_t NumberOfBatteryCells; + uint8_t TokenBatteryAlarm; // 0x8B + + union { + uint16_t AlarmsAsWord; + struct { + // High byte of alarms + bool Sensor2OvertemperatureAlarm :1; // 0x01 ??? + bool Sensor1Or2UndertemperatureAlarm :1; // 0x02 Disables charging, but Has no effect on discharging + bool CellOvervoltageAlarm :1; + bool CellUndervoltageAlarm :1; + bool _309_A_ProtectionAlarm :1; // 0x10 + bool _309_B_ProtectionAlarm :1; + bool Reserved1Alarm :1; + bool Reserved2Alarm :1; + + // Low byte of alarms + bool LowCapacityAlarm :1; // 0x01 + bool PowerMosFetOvertemperatureAlarm :1; + bool ChargeOvervoltageAlarm :1; + bool DischargeUndervoltageAlarm :1; + bool Sensor1Or2OvertemperatureAlarm :1; // 0x10 - Affects the charging/discharging MosFet state, not the enable flags + /* + * Set with delay of (Dis)ChargeOvercurrentDelaySeconds / "OCP Delay(S)" seconds initially or on retry. + * Retry is done after "OCPR Time(S)" + */ + bool ChargeOvercurrentAlarm :1; // 0x20 - Set with delay of ChargeOvercurrentDelaySeconds seconds initially or on retry + bool DischargeOvercurrentAlarm :1; // 0x40 - Set with delay of DischargeOvercurrentDelaySeconds seconds initially or on retry + bool CellVoltageDifferenceAlarm :1; // 0x80 + } AlarmBits; + } AlarmUnion; + + uint8_t TokenBatteryStatus; // 0x8C + union { + uint16_t StatusAsWord; + struct { + uint8_t ReservedStatusHighByte; // This is the low byte of StatusAsWord, but it was sent as high byte of status + bool ChargeMosFetActive :1; // 0x01 // Is disabled e.g. on over current or temperature + bool DischargeMosFetActive :1; // 0x02 // Is disabled e.g. on over current or temperature + bool BalancerActive :1; // 0x04 + bool BatteryDown :1; // 0x08 + uint8_t ReservedStatus :4; + } StatusBits; + } StatusUnion; + + uint8_t TokenBatteryOvervoltageProtection10Millivolt; // 0x8E + uint16_t BatteryOvervoltageProtection10Millivolt; // 1000 to 15000 + uint8_t TokenBatteryUndervoltageProtection10Millivolt; // 0x8F + uint16_t BatteryUndervoltageProtection10Millivolt; // 1000 to 15000 + uint8_t TokenCellOvervoltageProtectionMillivolt; // 0x90 + uint16_t CellOvervoltageProtectionMillivolt; // 1000 to 4500 + uint8_t TokenCellOvervoltageRecoveryMillivolt; // 0x91 + uint16_t CellOvervoltageRecoveryMillivolt; // 1000 to 4500 + uint8_t TokenCellOvervoltageDelaySeconds; // 0x92 + uint16_t CellOvervoltageDelaySeconds; // 1 to 60 + uint8_t TokenCellUndervoltageProtectionMillivolt; // 0x93 + uint16_t CellUndervoltageProtectionMillivolt; + uint8_t TokenCellUndervoltageRecoveryMillivolt; // 0x94 + uint16_t CellUndervoltageRecoveryMillivolt; + uint8_t TokenCellUndervoltageDelaySeconds; // 0x95 + uint16_t CellUndervoltageDelaySeconds; + + uint8_t TokenVoltageDifferenceProtectionMillivolt; // 0x96 + uint16_t VoltageDifferenceProtectionMillivolt; // 0 to 100 + + uint8_t TokenDischargeOvercurrentProtectionAmpere; // 0x97 + uint16_t DischargeOvercurrentProtectionAmpere; // 1 to 1000 + uint8_t TokenDischargeOvercurrentDelaySeconds; // 0x98 + uint16_t DischargeOvercurrentDelaySeconds; // 1 to 60 + uint8_t TokenChargeOvercurrentProtectionAmpere; // 0x99 + uint16_t ChargeOvercurrentProtectionAmpere; // 1 to 1000 + uint8_t TokenChargeOvercurrentDelaySeconds; // 0x9A + uint16_t ChargeOvercurrentDelaySeconds; // 1 to 60 + + uint8_t TokenBalancingStartVoltage; // 0x9B + uint16_t BalancingStartMillivolt; // 2000 to 4500 + uint8_t TokenBalancingStartDifferentialVoltage; // 0x9C + uint16_t BalancingStartDifferentialMillivolt; // 10 to 1000 + uint8_t TokenBalancingState; // 0x9D + uint8_t BalancingIsEnabled; // 0=off 1=on + + uint8_t TokenPowerMosFetTemperatureProtection; // 0x9E + uint16_t PowerMosFetTemperatureProtection; // 0 to 100 + uint8_t TokenPowerMosFetRecoveryTemperature; // 0x9F + uint16_t PowerMosFetRecoveryTemperature; // 0 to 100 + uint8_t TokenSensor1TemperatureProtection; // 0xA0 + uint16_t Sensor1TemperatureProtection; // 40 to 100 + uint8_t TokenSensor1RecoveryTemperature; // 0xA1 + uint16_t Sensor1RecoveryTemperature; // 40 to 100 + + uint8_t TokenBatteryDifferenceTemperatureProtection; // 0xA2 + uint16_t BatteryDifferenceTemperatureProtection; // 2 to 20 + + uint8_t TokenChargeOvertemperatureProtection; // 0xA3 + uint16_t ChargeOvertemperatureProtection; // 0 to 100 + uint8_t TokenDischargeOvertemperatureProtection; // 0xA4 + uint16_t DischargeOvertemperatureProtection; // 0 to 100 + + uint8_t TokenChargeUndertemperatureProtection; // 0xA5 + int16_t ChargeUndertemperatureProtection; // -45 to 25 + uint8_t TokenChargeRecoveryUndertemperature; // 0xA6 + int16_t ChargeRecoveryUndertemperature; // -45 to 25 + uint8_t TokenDischargeUndertemperatureProtection; // 0xA7 + int16_t DischargeUndertemperatureProtection; // -45 to 25 + uint8_t TokenDischargeRecoveryUndertemperature; // 0xA8 + int16_t DischargeRecoveryUndertemperature; // -45 to 25 + + uint8_t TokenBatteryCellCount; // 0xA9 + uint8_t BatteryCellCount; // 3 to 32 + + uint8_t TokenTotalCapacity; // 0xAA + uint32_t TotalCapacityAmpereHour; // Ah + + uint8_t TokenChargeMosFetState; // 0xAB + uint8_t ChargeIsEnabled; // 0=off 1=on + uint8_t TokenDischargeMosFetState; // 0xAC + uint8_t DischargeIsEnabled; // 0=off 1=on + + uint8_t TokenCurrentCalibration; // 0xAD + uint16_t CurrentCalibrationMilliampere; // 100 to 20000 mA - 1039 for my BMS (factory calibration?) + + uint8_t TokenBoardAddress; // 0xAE + uint8_t BoardAddress; // 1 -used for cascading + + uint8_t TokenBatteryType; // 0xAF + uint8_t BatteryType; // 0 (lithium iron phosphate), 1 (ternary), 2 (lithium titanate), value is constant 1 + + uint8_t TokenSleepWaitingTime; // 0xB0 + uint16_t SleepWaitingTimeSeconds; // + + uint8_t TokenLowCapacityAlarm; // 0xB1 + uint8_t LowCapacityAlarmPercent; // 0 to 80 + + uint8_t TokenModifyParameterPassword; // 0xB2 + char ModifyParameterPassword[10]; // "123456" - can be HEX + + uint8_t TokenDedicatedChargerSwitchState; // 0xB3 + uint8_t DedicatedChargerSwitchIsActive; // 0=off 1=on + + uint8_t TokenDeviceIdString; // 0xB4 + char DeviceIdString[8]; // First 8 characters of the manufacturer id entered in the app field "User Private Data" + + uint8_t TokenManufacturerDate; // 0xB5 + char ManufacturerDate[4]; // "YYMM" - Date of first connection with app + + uint8_t TokenSystemWorkingMinutes; // 0xB6 + uint32_t SystemWorkingMinutes; // Minutes + + uint8_t TokenSoftwareVersionNumber; // 0xB7 + char SoftwareVersionNumber[15]; // "11.XW_S11.26___" or from documentation: "NW_1_0_0_200428" + + uint8_t TokenStartCurrentCalibration; // 0xB8 + uint8_t StartCurrentCalibration; // 0=stop 1=start + + uint8_t TokenActualBatteryCapacity; // 0xB9 + uint32_t ActualBatteryCapacityAmpereHour; // Ah + + uint8_t TokenManufacturerId; // 0xBA + char ManufacturerId[24]; // First 12 characters of the 13 characters manufacturer id entered in the app field "User Private Data" + // followed by "JK_B2A20S20P" for my balancer + +// uint8_t TokenRestartSystem; // 0xBB +// uint8_t RestartSystem; // 0=stop 1=restart +// uint8_t TokenFactoryDataReset; // 0xBC +// uint8_t FactoryDataReset; // 0=stop 1=reset +// uint8_t TokenRemoteUpgradeIdentification; // 0xBD +// uint8_t RemoteUpgradeIdentification; // 0=stop 1=start +// +// uint8_t TokenGPSTurnOffVoltage; // 0xBE +// uint16_t GPSTurnOffVoltageMillivolt; +// uint8_t TokenGPSRecoveryVoltage; // 0xBF +// uint16_t GPSRecoveryVoltageMillivolt; + + uint8_t TokenProtocolVersionNumber; // 0xC0 + uint8_t ProtocolVersionNumber; // 00, 01 -> Redefine the 0x84 current data as 10 mA, + // with the highest bit being 0 for discharge and 1 for charge +}; + +#endif // _JK_BMS_H diff --git a/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino b/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino new file mode 100644 index 0000000..095c5b5 --- /dev/null +++ b/JK-BMSToPylontechCAN/JK-BMSToPylontechCAN.ino @@ -0,0 +1,705 @@ +/* + * JK-BMSToPylontechCAN.cpp + * + * Converts the JK-BMS RS485 data to Pylontech CAN data for inverters + * which are not compatible with JK-BMS protocol but with Pylontech protocol, like Deye inverters. + * It displays many BMS information and alarms on a locally attached 2004 LCD. + * The JK-BMS data are provided as RS232 TTL. + * + * The software TX and hardware RX lines are connected to the JK BMS and run with 115200 baud. + * CAN is connected to the inverter which must accept Pylontech low voltage protocol, which runs with 500 kBit/s. + * This protocol is used e.g by the Pylontech US2000 battery. + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * !!! 8 MHz crystal and 500 kBit/s does not work with MCP2515 !!! + * !!! So you must replace the crystal of the module with a 16 (or 20) MHz one !!! + * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + * + * Internal operation: + * 1. A request is sent to the BMS. + * 2. The BMS reply frame is stored in a buffer and parity and other plausi checks are made. + * 3. The cell data are converted and enhanced to fill the JKConvertedCellInfoStruct. + * 4. Other frame data are converted and enhanced to fill the JKComputedDataStruct. + * 5. The content of the result frame is printed. After reset, all info is printed once, then only dynamic info is printed. + * 6. The required CAN data is filled in the according PylontechCANFrameInfoStruct. + * 7. Dynamic data and errors are displayed on the optional 2004 LCD if attached. + * 8. CAN data is sent. + * + * The LCD has 3 "pages" showing overview data, up to 16 cell voltages, or SOC and current with big numbers. + * The pages can be switched by the button at pin 2. + * + * On timeout, the last BMS data is kept. + * + * It uses the following libraries, which are included in this repository: + * SoftwareSerialTX for sending Serial to JK-BMS + * Modified LiquidCrystal_I2C for I2C connected LCD + * LCDBigNumbers for LCD big number generation + * EasyButtonAtInt01 for LCD page switching button + * SoftI2CMaster for minimal I2C functions + * modified mcp_can_dfs.h file from Seed-Studio Seeed_Arduino_CAN + * + * Based on https://github.com/syssi/esphome-jk-bms and https://github.com/maxx-ukoo/jk-bms2pylontech + * + * Copyright (C) 2023 Armin Joachimsmeyer + * Email: armin.joachimsmeyer@gmail.com + * + * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * + * Arduino-Utils 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 . + * + UART-TTL + __________ _________ + | |<----- RX ----->| | + | JK-BMS |<----- TX ----->| UNO/ | + | |<----- GND ---->| NANO |<-- 5V + | | | |<-- GND + |__________| |_________| + + # UART-TTL socket (4 Pin, JST 1.25mm pitch) + ___ ________ ___ + | | + | O O O O | + |GND RX TX VBAT| + |________________| + | | | + | | ----- RX + | --------- D4 (or other) + --------------GND + */ + +#include + +#if !defined(LOCAL_DEBUG) +//#define LOCAL_DEBUG +#endif + +#define VERSION_EXAMPLE "1.0" + +#define BUZZER_PIN A2 // To signal errors + +// Debug stuff +#define DEBUG_PIN 8 // If low, print additional info +bool sDebugModeActive = false; + +/* + * Program timing + */ +#if defined(LOCAL_DEBUG) +#define MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS 5000 +#define MILLISECONDS_BETWEEN_CAN_FRAME_SEND 5000 +#else +#define MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS 2000 +#define MILLISECONDS_BETWEEN_CAN_FRAME_SEND 2000 +#endif +#define TIMEOUT_MILLIS_FOR_FRAME_REPLY 100 // I measured 15 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 sStaticInfoWasSent = false; // Flag to send static Info only once after reset. + +/* + * JK-BMS stuff + */ +#define MAXIMUM_NUMBER_OF_CELLS 24 // must be before #include "JK-BMS.h" +#include "JK-BMS.h" + +#include "SoftwareSerialTX.h" +SoftwareSerialTX TxToJKBMS(4); // Use a 115200 baud software serial for the short request frame +bool sFrameIsRequested = false; // If true, request was recently sent so now check for serial input +uint32_t sMillisOfLastRequestedFrame = -MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS; // // For BMS timing. Initial value to start first request immediately +uint32_t sMillisOfLastReceivedByte = 0; // For timeout +bool sFrameHasTimeout = false; // If true BMS is likely switched off. +uint16_t sFrameTimeoutCounter = 0; + +/* + * CAN stuff + */ +#include "Pylontech_CAN.h" // Must be before #include "MCP2515_TX.hpp" +//#define CRYSTAL_20MHZ_ASSEMBLED // Otherwise a 16 MHz crystal is assumed. Must be specified before #include "MCP2515_TX.hpp" +// 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 // Otherwise pin 9 is assumed. Must be specified before #include "MCP2515_TX.hpp" +#include "MCP2515_TX.hpp" // my reduced tx only driver +bool sCanDataIsInitialized = false; +uint32_t sMillisOfLastCANFrameSent = 0; // For CAN timing + +/* + * LCD stuff + */ +#define LCD_COLUMNS 20 +#define LCD_I2C_ADDRESS 0x27 // Default LCD address is 0x27 for a 20 chars and 4 line / 2004 display +#include "LiquidCrystal_I2C.hpp" // This defines USE_SOFT_I2C_MASTER, if SoftI2CMasterConfig.h is available. Use only the modified version delivered with this program! +LiquidCrystal_I2C myLCD(LCD_I2C_ADDRESS, LCD_COLUMNS, 4); +bool sSerialLCDAvailable; +/* + * Big numbers for LCD JK_BMS_PAGE_BIG_INFO page + */ +#define USE_SERIAL_2004_LCD // required by LCDBigNumbers.hpp +#include "LCDBigNumbers.hpp" // Include sources for LCD big number generation +LCDBigNumbers bigNumberLCD(&myLCD, BIG_NUMBERS_FONT_2_COLUMN_3_ROWS_VARIANT_1); // Use 2x3 numbers, 2. variant +const uint8_t bigNumbersTopBlock[8] PROGMEM = { 0x0F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // char 1: top block +const uint8_t bigNumbersBottomBlock[8] PROGMEM = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x0F }; // char 2: bottom block +/* + * Button for switching LCD pages + */ +#define USE_BUTTON_0 // Enable code for 1. button at INT0 / D2 +#include "EasyButtonAtInt01.hpp" +EasyButton Button0AtPin2; // Only 1. button (USE_BUTTON_0) enabled -> button is connected to INT0 +#define JK_BMS_PAGE_OVERVIEW 0 // is displayed in case of error +#define JK_BMS_PAGE_CELL_INFO 1 +#define JK_BMS_PAGE_BIG_INFO 2 +#define JK_BMS_PAGE_CAN_INFO 3 // Only if debug is enabled +#define JK_BMS_PAGE_MAX JK_BMS_PAGE_BIG_INFO +#define JK_BMS_PAGE_MAX_DEBUG JK_BMS_PAGE_CAN_INFO +//uint8_t sDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; // Start with Overview page +uint8_t sDisplayPageNumber = JK_BMS_PAGE_BIG_INFO; // Start with Big Info page + +void printBMSDataOnLCD(); +void LCDPrintSpaces(uint8_t aNumberOfSpacesToPrint); +void LCDClearLine(uint8_t aLineNumber); + +void processReceivedData(); + +//#define STANDALONE_TEST +#if defined(STANDALONE_TEST) // +uint8_t TestJKReplyStatusFrame[] PROGMEM = { /* Header*/0x4E, 0x57, 0x01, 0x2D, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x01, +/*Cell voltages*/0x79, 0x3C, 0x01, 0x0E, 0xED, 0x02, 0x0E, 0xFA, 0x03, 0x0E, 0xF7, 0x04, 0x0E, 0xEC, 0x05, 0x0E, 0xF8, 0x06, 0x0E, + 0xFA, 0x07, 0x0E, 0xF1, 0x08, 0x0E, 0xF8, 0x09, 0x0E, 0xE3, 0x0A, 0x0E, 0xFA, 0x0B, 0x0E, 0xF1, 0x0C, 0x0E, 0xFB, 0x0D, + 0x0E, 0xFB, 0x0E, 0x0E, 0xF2, 0x0F, 0x0E, 0xF2, 0x10, 0x0E, 0xF2, 0x11, 0x0E, 0xF2, 0x12, 0x0E, 0xF2, 0x13, 0x0E, 0xF2, + 0x14, 0x0E, 0xF2, + /*JKFrameAllDataStruct*/0x80, 0x00, 0x16, 0x81, 0x00, 0x15, 0x82, 0x00, 0x15, 0x83, 0x0F, 0xF8, 0x84, 0x80, 0xD0, 0x85, + 0x0F, 0x86, 0x02, 0x87, 0x00, 0x04, 0x89, 0x00, 0x00, 0x01, 0xE0, 0x8A, 0x00, 0x0E, 0x8B, 0x00, 0x00, 0x8C, 0x00, 0x07, + 0x8E, 0x16, 0x26, 0x8F, 0x10, 0xAE, 0x90, 0x0F, 0xD2, 0x91, 0x0F, 0xA0, 0x92, 0x00, 0x05, 0x93, 0x0B, 0xEA, 0x94, 0x0C, + 0x1C, 0x95, 0x00, 0x05, 0x96, 0x01, 0x2C, 0x97, 0x00, 0x07, 0x98, 0x00, 0x03, 0x99, 0x00, 0x05, 0x9A, 0x00, 0x05, 0x9B, + 0x0C, 0xE4, 0x9C, 0x00, 0x08, 0x9D, 0x01, 0x9E, 0x00, 0x5A, 0x9F, 0x00, 0x46, 0xA0, 0x00, 0x64, 0xA1, 0x00, 0x64, 0xA2, + 0x00, 0x14, 0xA3, 0x00, 0x46, 0xA4, 0x00, 0x46, 0xA5, 0xFF, 0xEC, 0xA6, 0xFF, 0xF6, 0xA7, 0xFF, 0xEC, 0xA8, 0xFF, 0xF6, + 0xA9, 0x0E, 0xAA, 0x00, 0x00, 0x00, 0x0E, 0xAB, 0x01, 0xAC, 0x01, 0xAD, 0x04, 0x11, 0xAE, 0x01, 0xAF, 0x01, 0xB0, 0x00, + 0x0A, 0xB1, 0x14, 0xB2, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x00, 0x00, 0x00, 0x00, 0xB3, 0x00, 0xB4, 0x49, 0x6E, 0x70, + 0x75, 0x74, 0x20, 0x55, 0x73, 0xB5, 0x32, 0x31, 0x30, 0x31, 0xB6, 0x00, 0x00, 0xE2, 0x00, 0xB7, 0x31, 0x31, 0x2E, 0x58, + 0x57, 0x5F, 0x53, 0x31, 0x31, 0x2E, 0x32, 0x36, 0x5F, 0x5F, 0x5F, 0xB8, 0x00, 0xB9, 0x00, 0x00, 0x04, 0x00, 0xBA, 0x49, + 0x6E, 0x70, 0x75, 0x74, 0x20, 0x55, 0x73, 0x65, 0x72, 0x64, 0x61, 0x4A, 0x4B, 0x5F, 0x42, 0x32, 0x41, 0x32, 0x30, 0x53, + 0x32, 0x30, 0x50, 0xC0, 0x01, + /*End*/0x00, 0x00, 0x00, 0x00, 0x68, 0x00, 0x00, 0x51, 0xC2 }; +#endif + +/* + * Helper macro for getting a macro definition as string + */ +#define STR_HELPER(x) #x +#define STR(x) STR_HELPER(x) + +void setup() { +// LED_BUILTIN pin is used as SPI Clock !!! +// pinMode(LED_BUILTIN, OUTPUT); +// digitalWrite(LED_BUILTIN, LOW); + + pinMode(DEBUG_PIN, INPUT_PULLUP); + + 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) +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__)); + + /* + * Initialize I2C and check for bus lockup + */ + if (!i2c_init()) { + Serial.println(F("I2C init failed")); + } + + /* + * Check for LCD connected + */ + sSerialLCDAvailable = i2c_start(LCD_I2C_ADDRESS << 1); + i2c_stop(); + + if (sSerialLCDAvailable) { + /* + * Print program, version and date on the upper two LCD lines + */ + myLCD.init(); + myLCD.clear(); + myLCD.backlight(); + myLCD.setCursor(0, 0); + myLCD.print(F("JK-BMS to CAN conv.")); + myLCD.setCursor(0, 1); + myLCD.print(F(VERSION_EXAMPLE " " __DATE__)); + bigNumberLCD.begin(); // This creates the custom character used for printing big numbers + + } else { + Serial.println(F("No I2C LCD connected at address " STR(LCD_I2C_ADDRESS))); + } + + /* + * 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 115200 bit/s!")); + if (sSerialLCDAvailable) { + myLCD.setCursor(0, 2); + myLCD.print(F("BMS serial started")); + } + + /* + * CAN initialization + */ +// if (CAN.begin(500000)) { // Resets the device and start the CAN bus at 500 kbps + byte tCanInitResult = initializeCAN(); + if (tCanInitResult == MCP2515_RETURN_OK) { // Resets the device and start the CAN bus at 500 kbps + Serial.println(F("CAN started with 500 kbit/s!")); + if (sSerialLCDAvailable) { + myLCD.setCursor(0, 3); + myLCD.print(F("CAN started")); + } + } else { + Serial.print(F("Starting CAN failed with result=")); + Serial.println(tCanInitResult); + if (sSerialLCDAvailable) { + myLCD.setCursor(0, 3); + myLCD.print(F("Starting CAN failed!")); + } + while (1) + ; + } + + /* + * Print debug pin info + */ + Serial.println(F("If you connect debug pin " STR(DEBUG_BUTTON_PIN) " to ground, additional debug data is printed")); + Serial.println(); + + if (sSerialLCDAvailable) { + delay(1000); // To see the date + myLCD.setCursor(0, 1); + myLCD.print(F("Debug pin = " STR(DEBUG_PIN) " ")); + + delay(2000); // To see the messages + myLCD.clear(); + } +} + +void loop() { + + sDebugModeActive = !digitalRead(DEBUG_PIN); + if (sSerialLCDAvailable) { + + /* + * Manually check button state change + */ + if (sFrameTimeoutCounter == 0 && Button0AtPin2.ButtonStateHasJustChanged) { +// reset flag in order to do call digitalWrite() only once per button press + Button0AtPin2.ButtonStateHasJustChanged = false; + if (Button0AtPin2.ButtonStateIsActive) { + sDisplayPageNumber++; + + if (sDisplayPageNumber == JK_BMS_PAGE_CELL_INFO) { + // Create symbols character for maximum and minimum + bigNumberLCD._createChar(1, bigNumbersTopBlock); + bigNumberLCD._createChar(2, bigNumbersBottomBlock); + + } else if (sDisplayPageNumber == JK_BMS_PAGE_BIG_INFO) { + // Creates custom character used for generating big numbers + bigNumberLCD.begin(); + + } else if ((sDebugModeActive && sDisplayPageNumber > JK_BMS_PAGE_MAX_DEBUG) + || (!sDebugModeActive && sDisplayPageNumber > JK_BMS_PAGE_MAX)) { + // wrap around + sDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; + sForcePrintUpTime = true; // force printing uptime + } + + Serial.print(F("Set LCD display page to: ")); + Serial.println(sDisplayPageNumber); + myLCD.clear(); + } + } + } + /* + * Request status frame every n seconds + */ + if (millis() - sMillisOfLastRequestedFrame >= MILLISECONDS_BETWEEN_JK_DATA_FRAME_REQUESTS) { + sMillisOfLastRequestedFrame = millis(); + /* + * Flush input buffer and send request to JK-BMS + */ + while (Serial.available()) { + Serial.read(); + } + requestJK_BMSStatusFrame(&TxToJKBMS, sDebugModeActive); + sFrameIsRequested = true; // enable check for serial input + initJKReplyFrameBuffer(); + sMillisOfLastReceivedByte = millis(); // initialize reply timeout + } + + if (sFrameIsRequested) { + if (Serial.available()) { + sMillisOfLastReceivedByte = millis(); + + uint8_t tReceiveResultCode = readJK_BMSStatusFrameByte(); + if (tReceiveResultCode != JK_BMS_RECEIVE_OK) { + if (tReceiveResultCode == JK_BMS_RECEIVE_FINISHED) { + /* + * All JK-BMS frame data received + */ + sFrameIsRequested = false; + if (sDebugModeActive) { + Serial.println(); + Serial.print(sReplyFrameBufferIndex + 1); + Serial.println(F(" bytes received")); + printJKReplyFrameBuffer(); + Serial.println(); + } + sFrameHasTimeout = false; + sFrameTimeoutCounter = 0; + sForcePrintUpTime = true; // to force printing on LCD + processReceivedData(); + } else { + /* + * Error here + */ + Serial.print(F("Receive error=")); + Serial.print(tReceiveResultCode); + Serial.print(F(" at index")); + Serial.println(sReplyFrameBufferIndex); + + sFrameIsRequested = false; + printJKReplyFrameBuffer(); + } + } + + } else if (millis() - sMillisOfLastReceivedByte >= TIMEOUT_MILLIS_FOR_FRAME_REPLY) { + /* + * Timeout for one byte + * If complete timeout, print it only once + */ + if (sReplyFrameBufferIndex != 0 || sFrameTimeoutCounter == 0 || sDebugModeActive) { + Serial.print(F("Receive timeout at sReplyFrameBufferIndex=")); + Serial.println(sReplyFrameBufferIndex); + if (sSerialLCDAvailable) { + myLCD.clear(); + myLCD.setCursor(0, 0); + myLCD.print(F("Receive timeout")); + } + } + + if (sSerialLCDAvailable) { + myLCD.setCursor(16, 0); + myLCD.print(sFrameTimeoutCounter); + } + + sFrameHasTimeout = true; + sFrameTimeoutCounter++; + + sFrameIsRequested = false; // do not try to receive more + +#if defined(STANDALONE_TEST) + /* + * Copy test data to receive buffer, if no data was received previously + */ + if (!sCanDataIsInitialized) { + Serial.println(F("Use fixed demo data")); + memcpy_P(JKReplyFrameBuffer, TestJKReplyStatusFrame, sizeof(TestJKReplyStatusFrame)); + sReplyFrameBufferIndex = sizeof(TestJKReplyStatusFrame) - 1; + printJKReplyFrameBuffer(); + Serial.println(); + processReceivedData(); + } +#endif + } + } + + /* + * Send CAN frame independently of the period of JK-BMS data requests + * 0.5 MB/s + * Inverter reply every second: 0x305: 00-00-00-00-00-00-00-00 + */ + if (sCanDataIsInitialized && millis() - sMillisOfLastCANFrameSent >= MILLISECONDS_BETWEEN_CAN_FRAME_SEND) { + sMillisOfLastCANFrameSent = millis(); + if (sDebugModeActive) { + Serial.println(F("Send CAN")); + } + sendPylontechAllCANFrames(sDebugModeActive); + + /* + * Signaling errors + */ + if (ErrorStringForLCD != NULL) { + tone(BUZZER_PIN, 2200, 50); + delay(200); + tone(BUZZER_PIN, 2200, 50); + } + } + +} + +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 (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(); + fillJKComputedData(); + + if (!sStaticInfoWasSent) { + sStaticInfoWasSent = true; + printJKStaticInfo(); + } + printJKDynamicInfo(); + Serial.println(); + fillAllCANData(sJKFAllReplyPointer); + sCanDataIsInitialized = true; + if (sSerialLCDAvailable) { + printBMSDataOnLCD(); + } +} +/* + * Print selected data on a 2004 LCD display + */ +void printBMSDataOnLCD() { + + myLCD.setCursor(0, 0); + if (ErrorStringForLCD != NULL) { + // print not more than 20 characters + char t20CharacterString[LCD_COLUMNS + 1]; + memcpy_P(t20CharacterString, ErrorStringForLCD, LCD_COLUMNS); + t20CharacterString[LCD_COLUMNS] = '\0'; + // Allow error flags to be seen on page CAN info + if (sDisplayPageNumber != JK_BMS_PAGE_CAN_INFO) { + sDisplayPageNumber = JK_BMS_PAGE_OVERVIEW; + myLCD.print(t20CharacterString); + } + } + + if (sDisplayPageNumber == JK_BMS_PAGE_OVERVIEW) { + /* + * Top line 0 - Error message or up time, which is only printed if changed + */ + if (ErrorStringForLCD == NULL && sForcePrintUpTime) { + sForcePrintUpTime = false; + myLCD.print(F("Uptime: ")); + myLCD.print(&sUpTimeString[4]); + } + + /* + * Line 1 - SOC and remaining capacity and state of MosFets or Error + */ + myLCD.setCursor(0, 1); +// Percent of charge + myLCD.print(sJKFAllReplyPointer->SOCPercent); + myLCD.print(F("% ")); + +// Remaining capacity + myLCD.print(JKComputedData.RemainingCapacityAmpereHour); + myLCD.print(F("Ah ")); + + if (sJKFAllReplyPointer->StatusUnion.StatusBits.ChargeMosFetActive) { + myLCD.print(F("CH ")); + } else { + myLCD.print(F(" ")); + } + + if (sJKFAllReplyPointer->StatusUnion.StatusBits.DischargeMosFetActive) { + myLCD.print(F("-CH ")); + } else { + myLCD.print(F(" ")); + } + + if (sJKFAllReplyPointer->StatusUnion.StatusBits.BalancerActive) { + myLCD.print(F("BAL")); + } else { + myLCD.print(F(" ")); + } + + /* + * Line 2 - Voltage, Current and Power + */ + myLCD.setCursor(0, 2); +// Voltage + uint_fast8_t tCharactersPrinted = myLCD.print(JKComputedData.BatteryVoltageFloat, 2); + myLCD.print(F("V ")); + +// Current + tCharactersPrinted += myLCD.print(JKComputedData.BatteryLoadCurrentFloat, 2); + myLCD.print(F("A ")); + +// Power + tCharactersPrinted += myLCD.print(JKComputedData.BatteryLoadPower); + myLCD.print('W'); + LCDPrintSpaces((LCD_COLUMNS - 5) - tCharactersPrinted); + + /* + * Line 3 - 3 Temperatures + */ + myLCD.setCursor(0, 3); + // 3 temperatures + myLCD.print(JKComputedData.TemperaturePowerMosFet); + myLCD.print(F("\xDF" "C ")); + myLCD.print(JKComputedData.TemperatureSensor1); + myLCD.print(F("\xDF" "C ")); + myLCD.print(JKComputedData.TemperatureSensor2); + myLCD.print(F("\xDF" "C ")); + + } else if (sDisplayPageNumber == JK_BMS_PAGE_CELL_INFO) { + uint_fast8_t tRowNumber; + if (JKConvertedCellInfo.NumberOfCellInfoEnties > 12) { + tRowNumber = 0; + } else { + myLCD.print(F(" -CELL INFO-")); + tRowNumber = 1; + } + for (uint8_t i = 0; i < JKConvertedCellInfo.NumberOfCellInfoEnties; ++i) { + if (i % 4 == 0) { + myLCD.setCursor(0, tRowNumber); + tRowNumber++; + } + if (i == JKConvertedCellInfo.MaximumVoltagCellIndex) { + myLCD.print((char) 0x1); + } else if (i == JKConvertedCellInfo.MinimumVoltagCellIndex) { + myLCD.print((char) 0x2); + } else { + myLCD.print(' '); + } + myLCD.print(JKConvertedCellInfo.CellInfoStructArray[i].CellMillivolt); + + } + } else if (sDisplayPageNumber == JK_BMS_PAGE_BIG_INFO) { + + bigNumberLCD.setBigNumberCursor(0, 0); + bigNumberLCD.print(sJKFAllReplyPointer->SOCPercent); + myLCD.setCursor(6, 2); + myLCD.print('%'); + + if (JKComputedData.BatteryLoadCurrentFloat < 0.0) { + // position the '-' with a space before the number + bigNumberLCD.writeAt('-', 7, 0); + bigNumberLCD.setBigNumberCursor(9, 0); + if (JKComputedData.BatteryLoadCurrentFloat < -10.0) { + bigNumberLCD.print(-JKComputedData.BatteryLoadCurrentFloat, 1); + } else { + bigNumberLCD.print(-JKComputedData.BatteryLoadCurrentFloat, 2); + } + + } else { + bigNumberLCD.setBigNumberCursor(9, 0); + if (JKComputedData.BatteryLoadCurrentFloat > 10.0) { + bigNumberLCD.print(JKComputedData.BatteryLoadCurrentFloat, 1); + } else { + bigNumberLCD.print(JKComputedData.BatteryLoadCurrentFloat, 2); + } + } + myLCD.setCursor(19, 2); + myLCD.print('A'); + + } else { + /* + * sDisplayPageNumber == JK_BMS_PAGE_CAN_INFO + */ + PylontechCANFrameStruct *tCANFrameDataPointer = + reinterpret_cast(&PylontechCANErrorsWarningsFrame); + if (tCANFrameDataPointer->FrameData.ULong.LowLong == 0) { + /* + * Caption and "No error" in row 1 and 2 + */ + myLCD.print(F(" -CAN INFO- ")); + if (PylontechCANSohSocFrame.FrameData.SOCPercent < 100) { + myLCD.print(' '); + } + myLCD.print(F("SOC=")); + myLCD.print(PylontechCANSohSocFrame.FrameData.SOCPercent); + myLCD.print('%'); + myLCD.setCursor(0, 1); + myLCD.print(F("No errors / warnings")); + + } else { + /* + * Errors and Warnings in row 1 and 2 + */ + myLCD.print(F("Errors: 0x")); + myLCD.print(tCANFrameDataPointer->FrameData.UBytes[0], HEX); + myLCD.print(F(" 0x")); + myLCD.print(tCANFrameDataPointer->FrameData.UBytes[1], HEX); + myLCD.setCursor(0, 1); + + myLCD.print(F("Warnings: 0x")); + myLCD.print(tCANFrameDataPointer->FrameData.UBytes[2], HEX); + myLCD.print(F(" 0x")); + myLCD.print(tCANFrameDataPointer->FrameData.UBytes[3], HEX); + } + + /* + * Voltage, current and maximum temperature in row 3 + */ + myLCD.setCursor(0, 2); + // Voltage + myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Voltage100Millivolt / 10); + myLCD.print('.'); + myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Voltage100Millivolt % 10); + myLCD.print(F("V ")); + + // Current + myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Current100Milliampere / 10); + myLCD.print('.'); + myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Current100Milliampere % 10); + myLCD.print(F("A ")); + + // Current + myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Temperature100Millicelsius / 10); + myLCD.print('.'); + myLCD.print(PylontechCANCurrentValuesFrame.FrameData.Temperature100Millicelsius % 10); + myLCD.print(F("\xDF" "C ")); + + /* + * Request flags in row 4 + */ + myLCD.setCursor(0, 3); + // Charge enable + if (PylontechCANBatteryRequesFrame.FrameData.ChargeEnable) { + myLCD.print(F("CH ")); + } else { + myLCD.print(F(" ")); + } + if (PylontechCANBatteryRequesFrame.FrameData.DischargeEnable) { + myLCD.print(F("-CH ")); + } else { + myLCD.print(F(" ")); + } + if (PylontechCANBatteryRequesFrame.FrameData.FullChargeRequest) { + myLCD.print(F("FULL ")); + } else { + myLCD.print(F(" ")); + } + if (PylontechCANBatteryRequesFrame.FrameData.ForceChargeRequestI + || PylontechCANBatteryRequesFrame.FrameData.ForceChargeRequestII) { + myLCD.print(F("FORCE ")); + } else { + myLCD.print(F(" ")); + } + + } +} + +void LCDPrintSpaces(uint8_t aNumberOfSpacesToPrint) { + for (uint_fast8_t i = 0; i < aNumberOfSpacesToPrint; ++i) { + myLCD.print(' '); + } +} + +void LCDClearLine(uint8_t aLineNumber) { + myLCD.setCursor(0, aLineNumber); + LCDPrintSpaces(20); + myLCD.setCursor(0, aLineNumber); +} diff --git a/JK-BMSToPylontechCAN/LCDBigNumbers.hpp b/JK-BMSToPylontechCAN/LCDBigNumbers.hpp new file mode 100644 index 0000000..f32a049 --- /dev/null +++ b/JK-BMSToPylontechCAN/LCDBigNumbers.hpp @@ -0,0 +1,783 @@ +/* + * LCDBigNumbers.hpp + * + * Arduino library to write big numbers on a 1602 or 2004 LCD. + * + * Copyright (C) 2022-2023 Armin Joachimsmeyer + * armin.joachimsmeyer@gmail.com + * + * This file is part of LCDBigNumbers https://github.com/ArminJo/LCDBigNumbers. + * + * LCDBigNumbers 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 _LCD_BIG_NUMBERS_HPP +#define _LCD_BIG_NUMBERS_HPP + +#include + +#define ONE_COLUMN_SPACE_CHARACTER '|' // This input character is printed as a one column space. Normal spaces are printed as a space with the width of the number. +#define ONE_COLUMN_SPACE_STRING "|" // This input string is printed as a one column space. Normal spaces are printed as a space with the width of the number. + +//#define USE_PARALLEL_2004_LCD // Is default +//#define USE_PARALLEL_1602_LCD +//#define USE_SERIAL_2004_LCD +//#define USE_SERIAL_1602_LCD + +#if !defined(USE_PARALLEL_2004_LCD) && !defined(USE_PARALLEL_1602_LCD) && !defined(USE_SERIAL_2004_LCD) && !defined(USE_SERIAL_1602_LCD) +#define USE_PARALLEL_2004_LCD // Use parallel 2004 LCD as default +#endif + +#if defined(USE_PARALLEL_2004_LCD) || defined(USE_PARALLEL_1602_LCD) +# if defined(USE_PARALLEL_2004_LCD) +#define LCD_COLUMNS 20 +#define LCD_ROWS 4 +# else +#define LCD_COLUMNS 16 +#define LCD_ROWS 2 +# endif +#define USE_PARALLEL_LCD +#include +#else +# if defined(USE_SERIAL_2004_LCD) +#define LCD_COLUMNS 20 +#define LCD_ROWS 4 +# else +#define LCD_COLUMNS 16 +#define LCD_ROWS 2 +# endif +#include "LiquidCrystal_I2C.h" // Use an up to date library version which has the init method +#endif + +#define DEFAULT_TEST_DELAY 3000 +#define NUMBER_OF_SPECIAL_CHARACTERS_IN_FONT_ARRAY 3 + +#define BIG_NUMBERS_FONT_1_COLUMN_2_ROWS_VARIANT_1 0x01 +#define BIG_NUMBERS_FONT_2_COLUMN_2_ROWS_VARIANT_1 0x05 +#define BIG_NUMBERS_FONT_3_COLUMN_2_ROWS_VARIANT_1 0x09 +#define BIG_NUMBERS_FONT_3_COLUMN_2_ROWS_VARIANT_2 0x19 +#define BIG_NUMBERS_FONT_3_COLUMN_2_ROWS_VARIANT_3 0x29 +#define BIG_NUMBERS_FONT_2_COLUMN_3_ROWS_VARIANT_1 0x06 +#define BIG_NUMBERS_FONT_2_COLUMN_3_ROWS_VARIANT_2 0x16 +#define BIG_NUMBERS_FONT_3_COLUMN_3_ROWS_VARIANT_1 0x0A +#define BIG_NUMBERS_FONT_3_COLUMN_4_ROWS_VARIANT_1 0x0B +#define BIG_NUMBERS_FONT_3_COLUMN_4_ROWS_VARIANT_2 0x1B +#define COLUMN_MASK 0x0C // Number of columns = shifted masked value + 1 +#define ROW_MASK 0x03 // Number of rows = masked value + 1 +#define VARIANT_MASK 0x30 + +//#define LOCAL_DEBUG // To debug/understand the writeBigNumber() function + +// !!! Must be without comment and closed by @formatter:on +// @formatter:off + +// http://www.picbasic.co.uk/forum/showthread.php?t=13376 +// 8 custom characters for 1 column font +const uint8_t bigNumbers1x2CustomPatterns_1[][8] PROGMEM = { + { B11110, B10010, B10010, B10010, B10010, B10010, B10010, B11110 }, // 0 Closed rectangle - 8 + { B11110, B10010, B10010, B10010, B10010, B10010, B10010, B10010 }, // 1 Rectangle - open at bottom - 0 + { B10010, B10010, B10010, B10010, B10010, B10010, B10010, B11110 }, // 2 Rectangle - open at top - 0 + { B11110, B00010, B00010, B00010, B00010, B00010, B00010, B11110 }, // 3 Rectangle - open at left + { B11110, B10000, B10000, B10000, B10000, B10000, B10000, B11110 }, // 4 Rectangle - open at right + { B00010, B00010, B00010, B00010, B00010, B00010, B00010, B00010 }, // 5 Right bar - 1 + { B11110, B00010, B00010, B00010, B00010, B00010, B00010, B00010 }, // 6 Top right - 7 + { B00010, B00010, B00010, B00010, B00010, B00010, B00010, B11110 } // 7 Right bottom - 3,5,9 +}; +const uint8_t bigNumbers1x2_1[2][13] PROGMEM = { // 2-line numbers +// "-" "." ":" 0 1 2 3 4 5 6 7 8 9 + { 0x5F, 0xFE, 0xA5, 0x01, 0x05, 0x06, 0x03, 0x02, 0x04, 0x04, 0x06, 0x00, 0x00 }, + { 0xFE, 0x2E, 0xA5, 0x02, 0x05, 0x04, 0x07, 0x05, 0x07, 0x02, 0x05, 0x02, 0x07 } +}; + +// https://www.alpenglowindustries.com/blog/the-big-numbers-go-marching-2x2#/ +// https://github.com/AlpenglowIndustries/Alpenglow_BigNums2x2 +// 8 custom characters for Trek font +const uint8_t bigNumbers2x2CustomPatterns_1[][8] PROGMEM = { + { B11111, B11111, B00000, B00000, B00000, B00000, B00000, B00000 }, + { B11000, B11000, B11000, B11000, B11000, B11000, B11000, B11000 }, + { B00000, B00000, B00000, B00000, B00000, B00000, B11111, B11111 }, + { B11111, B11111, B00011, B00011, B00011, B00011, B11111, B11111 }, + { B11111, B11111, B11000, B11000, B11000, B11000, B11111, B11111 }, + { B11111, B11111, B11000, B11000, B11000, B11000, B11000, B11000 }, + { B00011, B00011, B00011, B00011, B00011, B00011, B11111, B11111 }, + { B11000, B11000, B11000, B11000, B11000, B11000, B11111, B11111 } +}; +const uint8_t bigNumbers2x2_1[2][23] PROGMEM = { // 2-line numbers +// "-" "." ":" 0 1 2 3 4 5 6 7 8 9 + { 0xFE, 0xFE, 0xA5, 0x05,0xFF, 0x00,0x01, 0x00,0x03, 0x00,0x03, 0x01,0x01, 0x04,0x00, 0x05,0x00, 0x00,0x03, 0x04,0x03, 0x04,0x03}, + { 0x00, 0x2E, 0xA5, 0x07,0x06, 0x02,0x07, 0x04,0x02, 0x02,0x03, 0x00,0x05, 0x02,0x03, 0x04,0x03, 0xFE,0x01, 0x04,0x03, 0x02,0x06} +}; + + +// 3x2 https://liudr.wordpress.com/2011/03/21/big-font/ +// 3x2 http://www.netzmafia.de/skripten/hardware/Arduino/LCD/index.html +const uint8_t bigNumbers3x2CustomPatterns_1[6][8] PROGMEM = { + { B11111,B11111,B00000,B00000,B00000,B00000,B00000,B00000 }, // 0 Upper bar + { B00000,B00000,B00000,B00000,B00000,B00000,B11111,B11111 }, // 1 Lower bar + { B11111,B11111,B00000,B00000,B00000,B00000,B11111,B11111 }, // 2 Upper and lower bar + { B00000,B00000,B00000,B11111,B11111,B00000,B00000,B00000 }, // 3 Minus sign + { B00000,B00000,B00000,B00000,B00000,B01110,B01110,B01110 }, // 4 Decimal point + { B00000,B00000,B01110,B01110,B01110,B00000,B00000,B00000 } // 5 Colon +}; +const uint8_t bigNumbers3x2_1[2][33] PROGMEM = { // 2-line numbers +// "-" "." ":" 0 1 2 3 4 5 6 7 8 9 + { 0x01, 0xFE, 0x05, 0xFF,0x00,0xFF, 0x00,0xFF,0xFE, 0x02,0x02,0xFF, 0x00,0x02,0xFF, 0xFF,0x01,0xFF, 0xFF,0x02,0x02, 0xFF,0x02,0x02, 0x00,0x00,0xFF, 0xFF,0x02,0xFF, 0xFF,0x02,0xFF}, + { 0xFE, 0x04, 0x05, 0xFF,0x01,0xFF, 0x01,0xFF,0x01, 0xFF,0x01,0x01, 0x01,0x01,0xFF, 0xFE,0xFE,0xFF, 0x01,0x01,0xFF, 0xFF,0x01,0xFF, 0xFE,0xFE,0xFF, 0xFF,0x01,0xFF, 0x01,0x01,0xFF} +}; + +// 3x2 https://forum.arduino.cc/t/display-3-character-wide-big-digits-on-16x2-lcd/905360 +const uint8_t bigNumbers3x2CustomPatterns_2[8][8] PROGMEM = { + { B11111,B11111,B11111,B00000,B00000,B00000,B00000,B00000 }, // 0 Upper bar + { B00000,B00000,B00000,B00000,B00000,B11111,B11111,B11111 }, // 1 Lower bar + { B11111,B11111,B11111,B00000,B00000,B00000,B11111,B11111 }, // 2 Upper and lower bar + { B11100,B11100,B11100,B11100,B11100,B11100,B11100,B11100 }, // 3 Left bar + { B00000,B00000,B00000,B00000,B00000,B11100,B11100,B11100 }, // 4 Left lower bar for 2 + { B11100,B11100,B11100,B00000,B00000,B00000,B11100,B11100 }, // 5 Left upper and lower bar for 5,6 + { B00000,B00000,B00000,B00000,B00000,B01110,B01110,B01110 }, // 6 Decimal point + { B00000,B00000,B01110,B01110,B01110,B00000,B00000,B00000 } // 7 Colon +}; +const uint8_t bigNumbers3x2_2[2][33] PROGMEM = { // 2-line numbers +// "-" "." ":" 0 1 2 3 4 5 6 7 8 9 + { 0x01, 0xFE, 0x07, 0xFF,0x00,0x03, 0x00,0x03,0xFE, 0x02,0x02,0x03, 0x02,0x02,0x03, 0xFF,0x01,0x03, 0xFF,0x02,0x05, 0xFF,0x02,0x05, 0x00,0x00,0x03, 0xFF,0x02,0x03, 0xFF,0x02,0x03}, + { 0xFE, 0x06, 0x07, 0xFF,0x01,0x03, 0xFE,0x03,0xFE, 0xFF,0x01,0x04, 0x01,0x01,0x03, 0xFE,0xFE,0x03, 0x01,0x01,0x03, 0xFF,0x01,0x03, 0xFE,0xFE,0x03, 0xFF,0x01,0x03, 0xFE,0xFE,0x03} +}; + +//3x2 https://exploreembedded.com/wiki/Distance_Meter_with_Big_Fonts +const uint8_t bigNumbers3x2CustomPatterns_3[8][8] PROGMEM = { +{ B11100, B11110, B11110, B11110, B11110, B11110, B11110, B11100}, // 0 left bar +{ B00111, B01111, B01111, B01111, B01111, B01111, B01111, B00111}, // 1 right bar +{ B11111, B11111, B00000, B00000, B00000, B00000, B11111, B11111}, // 2 upper and lower bar +{ B11110, B11100, B00000, B00000, B00000, B00000, B11000, B11100}, // only in 5 +{ B01111, B00111, B00000, B00000, B00000, B00000, B00011, B00111}, // 4 +{ B00000, B00000, B00000, B00000, B00000, B00000, B11111, B11111}, +{ B00000, B00000, B00000, B00000, B00000, B00000, B00111, B01111}, // 6 +{ B11111, B11111, B00000, B00000, B00000, B00000, B00000, B00000} +}; +const uint8_t bigNumbers3x2_3[2][33] PROGMEM = { // 2-line numbers +// "-" "." ":" 0 1 2 3 4 5 6 7 8 9 + { 0xFE, 0xFE, 0xA5, 0x01,0x07,0x00, 0xFE,0x00,0xFE, 0x04,0x02,0x00, 0x04,0x02,0x00, 0x01,0x05,0x00, 0x01,0x02,0x03, 0x01,0x02,0x03, 0x01,0x07,0x00, 0x01,0x02,0x00, 0x01,0x02,0x00}, + { 0x07, 0x05, 0xA5, 0x01,0x05,0x00, 0xFE,0x00,0xFE, 0x01,0x05,0x05, 0x06,0x05,0x00, 0xFE,0xFE,0x00, 0x06,0x05,0x00, 0x01,0x05,0x00, 0xFE,0xFE,0x00, 0x01,0x05,0x00, 0x06,0x05,0x00} +}; + +// http://woodsgood.ca/projects/2015/01/16/large-numbers-on-small-displays/ +// 2x3 - Version 1 with space above +const uint8_t bigNumbers2x3CustomPatterns_1[8][8] PROGMEM = { { 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0x07 }, // char 0: bottom right + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1C, 0x1C }, // char 1: bottom left + { 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x1F, 0x1F }, // char 2: bottom block + { 0x07, 0x07, 0x07, 0x07, 0x07, 0x1F, 0x1F, 0x1F }, // char 3: right bottom block + { 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1F, 0x1F, 0x1F }, // char 4: left bottom block + { 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C }, // char 5: left bar + { 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07 }, // char 6: right bar + { 0x00, 0x00, 0x0E, 0x0E, 0x0E, 0x00, 0x00, 0x00 } // char 7: 3x3 block for colon +}; +const uint8_t bigNumbers2x3_1[3][23] PROGMEM = { // 3-line numbers +// "-" "." ":" 0 1 2 3 4 5 6 7 8 9 + { 0xFE, 0xFE, 0xFE, 0x02,0x02, 0x00,0x01, 0x02,0x02, 0x02,0x02, 0x01,0x00, 0x02,0x02, 0x01,0xFE, 0x02,0x02, 0x02,0x02, 0x02,0x02}, + { 0x02, 0xFE, 0x07, 0x05,0x06, 0xFE,0x05, 0x02,0x03, 0x00,0x03, 0x04,0x03, 0x04,0x02, 0x04,0x02, 0xFE,0x06, 0x04,0x03, 0x04,0x03}, + { 0xFE, 0x01, 0x07, 0x04,0x03, 0x00,0x04, 0x04,0x02, 0x02,0x03, 0xFE,0x06, 0x02,0x03, 0x04,0x03, 0xFE,0x06, 0x04,0x03, 0xFE,0x06} +}; + +// 2x3 - Version 2 with space below +const uint8_t bigNumbers2x3CustomPatterns_2[][8] PROGMEM = { { 0x07, 0x07, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00 }, // char 0: top right + { 0x1C, 0x1C, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x00 }, // char 1: top left + { 0x1F, 0x1F, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00 }, // char 2: top block + { 0x1F, 0x1F, 0x1F, 0x07, 0x07, 0x07, 0x07, 0x07 }, // char 3: right top block + { 0x1F, 0x1F, 0x1F, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C }, // char 4: left top block + { 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C }, // char 5: left bar + { 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07 }, // char 6: right bar + { 0x00, 0x00, 0x0E, 0x0E, 0x0E, 0x00, 0x00, 0x00 } // char 7: 3x3 block for colon +}; + +const uint8_t bigNumbers2x3_2[][23] PROGMEM = { // 3-line numbers +// "-" "." ":" 0 1 2 3 4 5 6 7 8 9 + { 0xFE, 0xFE, 0x07, 0x04,0x03, 0x00,0x05, 0x02,0x03, 0x02,0x03, 0x05,0x06, 0x04,0x02, 0x05,0xFE, 0x02,0x03, 0x04,0x03, 0x04,0x03}, + { 0x02, 0xFE, 0x07, 0x05,0x06, 0xFE,0x05, 0x04,0x02, 0x00,0x03, 0x02,0x03, 0x02,0x03, 0x04,0x03, 0xFE,0x06, 0x04,0x03, 0x02,0x03}, + { 0xFE, 0x00, 0xFE, 0x02,0x02, 0x00,0x02, 0x02,0x02, 0x02,0x02, 0xFE,0x00, 0x02,0x02, 0x02,0x02, 0xFE,0x00, 0x02,0x02, 0xFE,0x00} +}; + +// 3x4 Font custom patterns http://woodsgood.ca/projects/2015/03/06/3-4-line-big-font-numerals/ +const uint8_t bigNumbers3x3And3x4CustomPatterns_1[][8] PROGMEM = { { 0x01, 0x07, 0x0F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F }, // char 0: bottom right triangle + { 0x00, 0x00, 0x00, 0x00, 0x1F, 0x1F, 0x1F, 0x1F }, // char 1: bottom block + { 0x10, 0x1C, 0x1E, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F }, // char 2: bottom left triangle + { 0x1F, 0x0F, 0x07, 0x01, 0x00, 0x00, 0x00, 0x00 }, // char 3: top right triangle + { 0x1F, 0x1E, 0x1C, 0x10, 0x00, 0x00, 0x00, 0x00 }, // char 4: top left triangle + { 0x1F, 0x1F, 0x1F, 0x1F, 0x00, 0x00, 0x00, 0x00 }, // char 5: upper block + { 0x10, 0x1C, 0x1E, 0x1F, 0x00, 0x00, 0x00, 0x00 }, // char 6: full top left triangle Used only once in 7 +// { 0x1F, 0x1F, 0x1E, 0x1C, 0x18, 0x10, 0x00, 0x00 }, // char 6: full top left triangle Used only once in 7 + { 0x01, 0x07, 0x0F, 0x1F, 0x00, 0x00, 0x00, 0x00 } // char 7: top right triangle +}; + +const uint8_t bigNumbers3x3_1[3][33] PROGMEM = { // 3-line numbers +// "-" "." ":" 0 1 2 3 4 5 6 7 8 9 + { 0xFE, 0xFE, 0x01, 0x00,0x05,0x02, 0x07,0xFF,0xFE, 0x07,0x05,0x02, 0x07,0x05,0x02, 0xFF,0xFE,0xFF, 0xFF,0x05,0x05, 0x00,0x05,0x06, 0x05,0x05,0xFF, 0x00,0x05,0x02, 0x00,0x05,0x02}, + { 0x05, 0xFE, 0x01, 0xFF,0xFE,0xFF, 0xFE,0xFF,0xFE, 0x00,0x05,0x05, 0xFE,0x05,0xFF, 0x05,0x05,0xFF, 0x05,0x05,0xFF, 0xFF,0x05,0x02, 0xFE,0x00,0x04, 0xFF,0x05,0xFF, 0x03,0x05,0xFF}, + { 0xFE, 0x05, 0xFE, 0x03,0x05,0x04, 0xFE,0x05,0xFE, 0x05,0x05,0x05, 0x03,0x05,0x04, 0xFE,0xFE,0x05, 0x03,0x05,0x04, 0x03,0x05,0x04, 0xFE,0x05,0xFE, 0x03,0x05,0x04, 0x03,0x05,0x04} +}; + +// 3x4 Font variant 1 +const uint8_t bigNumbers3x4_1[4][33] PROGMEM = { // 4-line numbers +// "-" "." ":" 0 1 2 3 4 5 6 7 8 9 + { 0xFE, 0xFE, 0xFE, 0x00,0x05,0x02, 0x07,0xFF,0xFE, 0x07,0x05,0x02, 0x07,0x05,0x02, 0xFF,0xFE,0xFF, 0xFF,0x05,0x05, 0x00,0x05,0x02, 0x05,0x05,0xFF, 0x00,0x05,0x02, 0x00,0x05,0x02}, + { 0x01, 0xFE, 0x05, 0xFF,0xFE,0xFF, 0xFE,0xFF,0xFE, 0x01,0x01,0xFF, 0xFE,0x01,0xFF, 0xFF,0x01,0xFF, 0xFF,0x01,0x01, 0xFF,0x01,0x01, 0xFE,0x00,0x04, 0xFF,0x01,0xFF, 0xFF,0x01,0xFF}, + { 0xFE, 0xFE, 0x01, 0xFF,0xFE,0xFF, 0xFE,0xFF,0xFE, 0xFF,0xFE,0xFE, 0xFE,0xFE,0xFF, 0xFE,0xFE,0xFF, 0xFE,0xFE,0xFF, 0xFF,0xFE,0xFF, 0xFE,0xFF,0xFE, 0xFF,0xFE,0xFF, 0xFE,0xFE,0xFF}, + { 0xFE, 0x05, 0xFE, 0x03,0x05,0x04, 0xFE,0x05,0xFE, 0x05,0x05,0x05, 0x03,0x05,0x04, 0xFE,0xFE,0x05, 0x03,0x05,0x04, 0x03,0x05,0x04, 0xFE,0x05,0xFE, 0x03,0x05,0x04, 0x03,0x05,0x04} +}; + +// 3x4 Font variant 2 +// https://forum.arduino.cc/t/wie-bekommt-man-solch-grosse-zahlen-hin/986148/12 +const uint8_t bigNumbers3x4CustomPatterns_2[][8] PROGMEM = { +{ B00000, B00000, B00000, B00000, B00001, B00111, B01111, B11111 }, // char 0: bottom right triangle +{ B00000, B00000, B00000, B00000, B11111, B11111, B11111, B11111 }, // char 1: bottom block +{ B00000, B00000, B00000, B00000, B10000, B11100, B11110, B11111 }, // char 2: bottom left triangle +{ B11111, B01111, B00111, B00001, B00000, B00000, B00000, B00000 }, // char 3: top right triangle +{ B11111, B11111, B11111, B11111, B00000, B00000, B00000, B00000 }, // char 4: upper block +{ B11111, B11110, B11100, B10000, B00000, B00000, B00000, B00000 }, // char 5: top left triangle +{ B11111, B11111, B11111, B11111, B11111, B01111, B00111, B00001 }, // char 6: full top right triangle +{ B11111, B11111, B11111, B11111, B11111, B11110, B11100, B10000 } // char 7: full top left triangle +}; + +const uint8_t bigNumbers3x4_2[4][33] PROGMEM = { // 4-line numbers +// "-" "." ":" 0 1 2 3 4 5 6 7 8 9 + { 0xFE, 0xFE, 0xFE, 0x00,0x01,0x02, 0x00,0x01,0xFE, 0x00,0x01,0x02, 0x00,0x01,0x02, 0x00,0xFE,0x01, 0x01,0x01,0x01, 0x00,0x01,0x02, 0x01,0x01,0x01, 0x00,0x01,0x02, 0x00,0x01,0x02}, + { 0x01, 0xFE, 0x04, 0xFF,0x00,0xFF, 0x05,0xFF,0xFE, 0x04,0x00,0x07, 0x04,0x00,0x07, 0xFF,0xFE,0xFF, 0x06,0x01,0x02, 0xFF,0x01,0x02, 0xFE,0x00,0x07, 0x06,0x01,0x07, 0x06,0x01,0xFF}, + { 0xFE, 0xFE, 0x01, 0xFF,0x05,0xFF, 0xFE,0xFF,0xFE, 0xFF,0x05,0xFE, 0x01,0x03,0xFF, 0x04,0x04,0xFF, 0x01,0xFE,0xFF, 0xFF,0xFE,0xFF, 0xFF,0x05,0xFE, 0xFF,0xFE,0xFF, 0xFE,0xFE,0xFF}, + { 0xFE, 0x04, 0xFE, 0x03,0x04,0x05, 0xFE,0x04,0xFE, 0x04,0x04,0x04, 0x03,0x04,0x05, 0xFE,0xFE,0x04, 0x03,0x04,0x05, 0x03,0x04,0x05, 0x04,0xFE,0xFE, 0x03,0x04,0x05, 0x03,0x04,0x05} +}; + +// 4x4: https://github.com/wa1hco/BigFont +// @formatter:on +class LCDBigNumbers: public Print { + +public: +#if defined(USE_PARALLEL_LCD) + LiquidCrystal *LCD; +#else + LiquidCrystal_I2C *LCD; +#endif + uint8_t NumberWidth; + uint8_t NumberHeight; + const uint8_t (*bigNumbersCustomPatterns)[8]; + uint8_t NumberOfCustomPatterns; + const uint8_t *bigNumbersFont; + bool forceGapBetweenNumbers; + uint8_t upperLeftColumnIndex; + uint8_t upperLeftRowIndex; + + /* + * + */ + void setBigNumberCursor(uint8_t aUpperLeftColumnIndex, uint8_t aUpperLeftRowIndex = 0) { + upperLeftColumnIndex = aUpperLeftColumnIndex; + upperLeftRowIndex = aUpperLeftRowIndex; + } + + size_t write(uint8_t aBigNumberValue) { + writeBigNumber(aBigNumberValue); + return 1; // assume success + } + + /* + * Creates custom character used for generating big numbers + */ + void begin() { + // create (8) custom characters + for (uint_fast8_t i = 0; i < NumberOfCustomPatterns; i++) { + _createChar(i, bigNumbersCustomPatterns[i]); + } + } + + void enableGapBetweenNumbers() { + forceGapBetweenNumbers = true; + } + void disableGapBetweenNumbers() { + forceGapBetweenNumbers = false; + } + void setGapBetweenNumbers(bool aForceGapBetweenNumbers) { + forceGapBetweenNumbers = aForceGapBetweenNumbers; + } + + /* + * Internal function to select the appropriate font arrays + */ + void init(const uint8_t aBigNumberFontIdentifier) { + setBigNumberCursor(0); + forceGapBetweenNumbers = true; + NumberWidth = ((aBigNumberFontIdentifier & COLUMN_MASK) >> 2) + 1; + NumberHeight = (aBigNumberFontIdentifier & ROW_MASK) + 1; + NumberOfCustomPatterns = 8; + switch (aBigNumberFontIdentifier) { + case BIG_NUMBERS_FONT_1_COLUMN_2_ROWS_VARIANT_1: + bigNumbersCustomPatterns = bigNumbers1x2CustomPatterns_1; + bigNumbersFont = (const uint8_t*) bigNumbers1x2_1; + forceGapBetweenNumbers = false; + break; + case BIG_NUMBERS_FONT_2_COLUMN_2_ROWS_VARIANT_1: + bigNumbersCustomPatterns = bigNumbers2x2CustomPatterns_1; + bigNumbersFont = (const uint8_t*) bigNumbers2x2_1; + break; + case BIG_NUMBERS_FONT_3_COLUMN_2_ROWS_VARIANT_1: + bigNumbersCustomPatterns = bigNumbers3x2CustomPatterns_1; + bigNumbersFont = (const uint8_t*) bigNumbers3x2_1; + NumberOfCustomPatterns = 6; + break; + case BIG_NUMBERS_FONT_3_COLUMN_2_ROWS_VARIANT_2: + bigNumbersCustomPatterns = bigNumbers3x2CustomPatterns_3; + bigNumbersFont = (const uint8_t*) bigNumbers3x2_3; + forceGapBetweenNumbers = false; + break; + case BIG_NUMBERS_FONT_3_COLUMN_2_ROWS_VARIANT_3: + bigNumbersCustomPatterns = bigNumbers3x2CustomPatterns_2; + bigNumbersFont = (const uint8_t*) bigNumbers3x2_2; + forceGapBetweenNumbers = false; + break; +#if LCD_ROWS <= 2 + default: + // ERROR: NumberHeight is greater than 2 for a 2 line display -> fallback to 2x2 font + bigNumbersCustomPatterns = bigNumbers2x2CustomPatterns_1; + bigNumbersFont = (const uint8_t*) bigNumbers2x2_1; + break; +#else + case BIG_NUMBERS_FONT_2_COLUMN_3_ROWS_VARIANT_1: + bigNumbersCustomPatterns = bigNumbers2x3CustomPatterns_1; + bigNumbersFont = (const uint8_t*) bigNumbers2x3_1; + break; + case BIG_NUMBERS_FONT_2_COLUMN_3_ROWS_VARIANT_2: + bigNumbersCustomPatterns = bigNumbers2x3CustomPatterns_2; + bigNumbersFont = (const uint8_t*) bigNumbers2x3_2; + break; + case BIG_NUMBERS_FONT_3_COLUMN_3_ROWS_VARIANT_1: + bigNumbersCustomPatterns = bigNumbers3x3And3x4CustomPatterns_1; + bigNumbersFont = (const uint8_t*) bigNumbers3x3_1; + break; + case BIG_NUMBERS_FONT_3_COLUMN_4_ROWS_VARIANT_1: + bigNumbersCustomPatterns = bigNumbers3x3And3x4CustomPatterns_1; + bigNumbersFont = (const uint8_t*) bigNumbers3x4_1; + break; + case BIG_NUMBERS_FONT_3_COLUMN_4_ROWS_VARIANT_2: + bigNumbersCustomPatterns = bigNumbers3x4CustomPatterns_2; + bigNumbersFont = (const uint8_t*) bigNumbers3x4_2; + break; +#endif + + } + } + +#if defined(USE_PARALLEL_LCD) + LCDBigNumbers(LiquidCrystal *aLCD, const uint8_t aBigNumberFontIdentifier) : +#else + LCDBigNumbers(LiquidCrystal_I2C *aLCD, const uint8_t aBigNumberFontIdentifier) : +#endif + LCD(aLCD) { + init(aBigNumberFontIdentifier); + } + + //createChar with PROGMEM input + void _createChar(uint8_t location, const uint8_t *charmap) { + location &= 0x7; // we only have 8 locations 0-7 + LCD->command(LCD_SETCGRAMADDR | (location << 3)); + for (int i = 0; i < 8; i++) { + LCD->write(pgm_read_byte(charmap++)); + } + } + + /** + * Draws a big digit of size aNumberWidth x aNumberHeight at cursor position + * Special characters always have the width of 1! + * After each number one column gap is inserted. The gap is not cleared! + * @param aNumber - Number or one of " ", "-", "." and ":" special characters to display + */ + void writeBigNumber(uint8_t aNumber) { + uint_fast8_t tCharacterWidth; + uint_fast8_t tFontArrayOffset = 0; + /* + * First 3 entries are the special characters + */ + if (aNumber == '-') { + tCharacterWidth = 1; + } else if (aNumber == '.') { + tFontArrayOffset = 1; + tCharacterWidth = 1; + } else if (aNumber == ':') { + tFontArrayOffset = 2; + tCharacterWidth = 1; + } else if (aNumber == ' ') { + tCharacterWidth = NumberWidth; + } else if (aNumber == ONE_COLUMN_SPACE_CHARACTER) { + // print a one column space + tCharacterWidth = 1; + aNumber = ' '; + } else { + if (aNumber > 9) { + aNumber -= '0'; // convert ASCII value to number + } + if (aNumber > 9) { + aNumber = ' '; // convert all non numbers to spaces with the width of the number + } + tCharacterWidth = NumberWidth; + tFontArrayOffset = NUMBER_OF_SPECIAL_CHARACTERS_IN_FONT_ARRAY + (aNumber * tCharacterWidth); + } +#if defined(LOCAL_DEBUG) + Serial.print(F("Number=")); + Serial.print(aNumber); + Serial.print(F(" CharacterWidth=")); + Serial.print(tCharacterWidth); + Serial.print(F(" FontArrayOffset=")); + Serial.print(tFontArrayOffset); + Serial.print(F(" ColunmOffset=")); + Serial.println(upperLeftColumnIndex); +#endif + const uint8_t *tArrayPtr = bigNumbersFont + tFontArrayOffset; + for (uint_fast8_t tRow = 0; tRow < NumberHeight; tRow++) { + LCD->setCursor(upperLeftColumnIndex, upperLeftRowIndex + tRow); + for (uint_fast8_t i = 0; i < tCharacterWidth; i++) { + uint8_t tCharacterIndex; + if (aNumber == ' ') { + tCharacterIndex = aNumber; // Blank + } else { + tCharacterIndex = pgm_read_byte(tArrayPtr); + } + LCD->write(tCharacterIndex); + tArrayPtr++; // next number column +#if defined(LOCAL_DEBUG) + Serial.print(F(" 0x")); + Serial.print(tCharacterIndex, HEX); + Serial.print(F(" 0x")); + Serial.print((uint16_t) tArrayPtr, HEX); +#endif + } + tArrayPtr += NUMBER_OF_SPECIAL_CHARACTERS_IN_FONT_ARRAY + (NumberWidth - tCharacterWidth) + (9 * NumberWidth); // Next array row +#if defined(LOCAL_DEBUG) + Serial.print('|'); +#endif + } + upperLeftColumnIndex += tCharacterWidth; + + if (forceGapBetweenNumbers && tCharacterWidth > 1) { + upperLeftColumnIndex++; // This provides one column gap between big numbers, but not between special characters. The gap is not cleared! + } + +#if defined(LOCAL_DEBUG) + Serial.println(); +#endif + } + /** + * Draws a big digit of size aNumberWidth x aNumberHeight + * @param aNumber - Number to display, if > 9 a blank character is drawn + * @param aUpperLeftColumnIndex - Starts with 0, no check! + * @param aStartRowIndex - Starts with 0, no check! + */ + void writeAt(uint8_t aNumber, uint8_t aUpperLeftColumnIndex, uint8_t aUpperLeftRowIndex = 0) { + setBigNumberCursor(aUpperLeftColumnIndex, aUpperLeftRowIndex); + writeBigNumber(aNumber); + } + +}; + +#if defined(USE_PARALLEL_LCD) +void printSpaces(LiquidCrystal *aLCD, uint_fast8_t aNumberOfSpacesToPrint) +#else +void printSpaces(LiquidCrystal_I2C *aLCD, uint_fast8_t aNumberOfSpacesToPrint) +#endif + { + for (uint_fast8_t i = 0; i < aNumberOfSpacesToPrint; ++i) { + aLCD->print(' '); + } +} + +#if defined(USE_PARALLEL_LCD) +void clearLine(LiquidCrystal *aLCD, uint_fast8_t aLineNumber) +#else +void clearLine(LiquidCrystal_I2C *aLCD, uint_fast8_t aLineNumber) +#endif + { + aLCD->setCursor(0, aLineNumber); + printSpaces(aLCD, LCD_COLUMNS); + aLCD->setCursor(0, aLineNumber); +} + +#if defined(USE_PARALLEL_LCD) +size_t printHex(LiquidCrystal *aLCD, uint16_t aHexByteValue) +#else +size_t printHex(LiquidCrystal_I2C *aLCD, uint16_t aHexByteValue) +#endif + { + aLCD->print(F("0x")); + size_t tPrintSize = 2; + if (aHexByteValue < 0x10 || (aHexByteValue > 0x100 && aHexByteValue < 0x1000)) { + aLCD->print('0'); // leading 0 + tPrintSize++; + } + return aLCD->print(aHexByteValue, HEX) + tPrintSize; +} + +/* + * On my 2004 LCD the custom characters are available under 0 to 7 and mirrored to 8 to 15 + * The characters 0x80 to 0x8F are blanks + */ +#if defined(USE_PARALLEL_LCD) +void showSpecialCharacters(LiquidCrystal *aLCD) +#else +void showSpecialCharacters(LiquidCrystal_I2C *aLCD) +#endif + { + aLCD->setCursor(0, 0); + // 0 to 7 are mirrored to 8 to 15 as described in datasheet + for (uint_fast8_t i = 0; i < 0x8; ++i) { + aLCD->write(i); + } + // Print some interesting characters + aLCD->write(0xA1); + aLCD->write(0xA5); + aLCD->write(0xB0); + aLCD->write(0xDB); + aLCD->write(0xDF); + + aLCD->setCursor(0, 1); + // The characters 0x10 to 0x1F seem to be all blanks => ROM Code: A00 + for (uint_fast8_t i = 0x10; i < 0x20; ++i) { + aLCD->write(i); + } + aLCD->setCursor(0, 2); + // The characters 0x80 to 0x8F seem to be all blanks => ROM Code: A00 + for (uint_fast8_t i = 0x80; i < 0x90; ++i) { + aLCD->write(i); + } + aLCD->setCursor(0, 3); + // The characters 0x90 to 0x9F seem to be all blanks => ROM Code: A00 + for (uint_fast8_t i = 0x90; i < 0xA0; ++i) { + aLCD->write(i); + } + delay(2000); +} + +#if defined(USE_PARALLEL_LCD) +void showCustomCharacters(LiquidCrystal *aLCD) +#else +void showCustomCharacters(LiquidCrystal_I2C *aLCD) +#endif + { + aLCD->setCursor(0, 0); + for (uint_fast8_t i = 0; i < 0x08; ++i) { + aLCD->write(i); + } +} + +#if defined(USE_PARALLEL_LCD) +/* + * Print all fonts, used in screenshots, using one object + */ +void testBigNumbers(LiquidCrystal *aLCD) +#else +void testBigNumbers(LiquidCrystal_I2C *aLCD) +#endif + { + /* + * 1 X 2 + */ + aLCD->clear(); // Clear display + // Allocate object + LCDBigNumbers bigNumberLCD(aLCD, BIG_NUMBERS_FONT_1_COLUMN_2_ROWS_VARIANT_1); + bigNumberLCD.begin(); // Generate font symbols in LCD controller + bigNumberLCD.print(F("0123456789 -.:")); // no special space required, we have an 1 column font + delay(DEFAULT_TEST_DELAY); + + /* + * 2 X 2 + */ + aLCD->clear(); // Clear display + // Reconfigure existing object to hold another font + bigNumberLCD.init(BIG_NUMBERS_FONT_2_COLUMN_2_ROWS_VARIANT_1); + bigNumberLCD.begin(); // Generate font symbols in LCD controller + bigNumberLCD.print(F("01234")); +#if LCD_ROWS <= 2 + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("56789")); + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("-.: ")); +#else + bigNumberLCD.setBigNumberCursor(0, 2); + bigNumberLCD.print(F("56789" ONE_COLUMN_SPACE_STRING "-.:")); +#endif + delay(DEFAULT_TEST_DELAY); + + /* + * 3 X 2 + */ + aLCD->clear(); // Clear display + bigNumberLCD.init(BIG_NUMBERS_FONT_3_COLUMN_2_ROWS_VARIANT_1); + bigNumberLCD.begin(); + +#if LCD_ROWS <= 2 + bigNumberLCD.print(F("0123")); + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("4567")); + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("89" ONE_COLUMN_SPACE_STRING "-.: ")); +#else + bigNumberLCD.print(F("01234")); + bigNumberLCD.setBigNumberCursor(0, 2); + bigNumberLCD.print(F("56789")); + delay(DEFAULT_TEST_DELAY); + + aLCD->clear(); // Clear display + // Print "-47.11 :" + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("--" ONE_COLUMN_SPACE_STRING "47.11")); + bigNumberLCD.writeAt(':', 19); // Keep in mind that numbers always have a trailing but no leading gap. +#endif + + delay(DEFAULT_TEST_DELAY); + + /* + * 3 X 2 2. variant + */ + aLCD->clear(); // Clear display + bigNumberLCD.init( BIG_NUMBERS_FONT_3_COLUMN_2_ROWS_VARIANT_2); + bigNumberLCD.begin(); + bigNumberLCD.print(F("01234")); +#if LCD_ROWS <= 2 + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("56789")); + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("-.: ")); +#else + bigNumberLCD.setBigNumberCursor(0, 2); + bigNumberLCD.print(F("56789" ONE_COLUMN_SPACE_STRING "-.:")); +#endif + delay(DEFAULT_TEST_DELAY); + + /* + * 3 X 2 3. variant + */ + aLCD->clear(); // Clear display + bigNumberLCD.init( BIG_NUMBERS_FONT_3_COLUMN_2_ROWS_VARIANT_3); + bigNumberLCD.begin(); + bigNumberLCD.print(F("01234")); +#if LCD_ROWS <= 2 + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("56789")); + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("-.: ")); +#else + bigNumberLCD.setBigNumberCursor(0, 2); + bigNumberLCD.print(F("56789" ONE_COLUMN_SPACE_STRING "-.:")); +#endif + delay(DEFAULT_TEST_DELAY); + +#if LCD_ROWS > 2 + /**************** + * 3 line numbers + ****************/ + /* + * 2 X 3 Space above + */ + aLCD->clear(); // Clear display + bigNumberLCD.init(BIG_NUMBERS_FONT_2_COLUMN_3_ROWS_VARIANT_1); + bigNumberLCD.begin(); + bigNumberLCD.setBigNumberCursor(0, 1); + bigNumberLCD.print(F("01234")); + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0, 1); + bigNumberLCD.print(F("56789" ONE_COLUMN_SPACE_STRING "-.:")); + delay(DEFAULT_TEST_DELAY); + + /* + * 2 X 3 Space below + */ + aLCD->clear(); // Clear display + bigNumberLCD.init( BIG_NUMBERS_FONT_2_COLUMN_3_ROWS_VARIANT_2); + bigNumberLCD.begin(); + bigNumberLCD.setBigNumberCursor(0, 1); + bigNumberLCD.print(F("01234")); + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0, 1); + bigNumberLCD.print(F("56789" ONE_COLUMN_SPACE_STRING "-.:")); + delay(DEFAULT_TEST_DELAY); + + /* + * 3 X 3 Space below + */ + aLCD->clear(); // Clear display + bigNumberLCD.init(BIG_NUMBERS_FONT_3_COLUMN_3_ROWS_VARIANT_1); + bigNumberLCD.begin(); + bigNumberLCD.setBigNumberCursor(0, 1); + bigNumberLCD.print(F("01234")); + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0, 1); + bigNumberLCD.print(F("56789")); + delay(DEFAULT_TEST_DELAY); + + aLCD->clear(); // Clear display + // Print "-47.11 :" + bigNumberLCD.setBigNumberCursor(0, 1); + bigNumberLCD.print(F("--" ONE_COLUMN_SPACE_STRING "47.11")); + bigNumberLCD.writeAt(':', 19, 1); // Keep in mind that numbers always have a trailing but no leading gap. + delay(DEFAULT_TEST_DELAY); + + /**************** + * 4 line numbers + ****************/ + /* + * 3 X 4 + */ + aLCD->clear(); // Clear display + bigNumberLCD.init( BIG_NUMBERS_FONT_3_COLUMN_4_ROWS_VARIANT_1); + bigNumberLCD.begin(); + bigNumberLCD.print(F("01234")); + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("56789")); + delay(DEFAULT_TEST_DELAY); + + aLCD->clear(); // Clear display + // Print "-47.11 :" + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("--" ONE_COLUMN_SPACE_STRING "47.11")); + bigNumberLCD.writeAt(':', 19); // Keep in mind that numbers always have a trailing but no leading gap. + delay(DEFAULT_TEST_DELAY); + + /* + * Variant 2 + */ + aLCD->clear(); // Clear display + bigNumberLCD.init( BIG_NUMBERS_FONT_3_COLUMN_4_ROWS_VARIANT_2); + bigNumberLCD.begin(); + bigNumberLCD.print(F("01234")); + delay(DEFAULT_TEST_DELAY); + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("56789")); + delay(DEFAULT_TEST_DELAY); + + aLCD->clear(); // Clear display + // Print "-47.11 :" + bigNumberLCD.setBigNumberCursor(0); + bigNumberLCD.print(F("--" ONE_COLUMN_SPACE_STRING "47.11")); + bigNumberLCD.writeAt(':', 19); // Keep in mind that numbers always have a trailing but no leading gap. + delay(DEFAULT_TEST_DELAY); +#endif // LCD_ROWS > 2 +} + +#if defined(LOCAL_DEBUG) +#undef LOCAL_DEBUG +#endif +#endif // _LCD_BIG_NUMBERS_HPP diff --git a/JK-BMSToPylontechCAN/LiquidCrystal_I2C.h b/JK-BMSToPylontechCAN/LiquidCrystal_I2C.h new file mode 100644 index 0000000..420214a --- /dev/null +++ b/JK-BMSToPylontechCAN/LiquidCrystal_I2C.h @@ -0,0 +1,129 @@ +//YWROBOT +#ifndef LiquidCrystal_I2C_h +#define LiquidCrystal_I2C_h + +#include +#include "Print.h" +#if !defined(USE_SOFT_I2C_MASTER) && !defined(USE_SOFT_WIRE) +#include +#endif + +// commands +#define LCD_CLEARDISPLAY 0x01 +#define LCD_RETURNHOME 0x02 +#define LCD_ENTRYMODESET 0x04 +#define LCD_DISPLAYCONTROL 0x08 +#define LCD_CURSORSHIFT 0x10 +#define LCD_FUNCTIONSET 0x20 +#define LCD_SETCGRAMADDR 0x40 +#define LCD_SETDDRAMADDR 0x80 + +// flags for display entry mode +#define LCD_ENTRYRIGHT 0x00 +#define LCD_ENTRYLEFT 0x02 +#define LCD_ENTRYSHIFTINCREMENT 0x01 +#define LCD_ENTRYSHIFTDECREMENT 0x00 + +// flags for display on/off control +#define LCD_DISPLAYON 0x04 +#define LCD_DISPLAYOFF 0x00 +#define LCD_CURSORON 0x02 +#define LCD_CURSOROFF 0x00 +#define LCD_BLINKON 0x01 +#define LCD_BLINKOFF 0x00 + +// flags for display/cursor shift +#define LCD_DISPLAYMOVE 0x08 +#define LCD_CURSORMOVE 0x00 +#define LCD_MOVERIGHT 0x04 +#define LCD_MOVELEFT 0x00 + +// flags for function set +#define LCD_8BITMODE 0x10 +#define LCD_4BITMODE 0x00 +#define LCD_2LINE 0x08 +#define LCD_1LINE 0x00 +#define LCD_5x10DOTS 0x04 +#define LCD_5x8DOTS 0x00 + +// flags for backlight control +#define LCD_BACKLIGHT 0x08 +#define LCD_NOBACKLIGHT 0x00 + +#define En 0b00000100 // Enable bit +#define Rw 0b00000010 // Read/Write bit +#define Rs 0b00000001 // Register select bit + +class LiquidCrystal_I2C : public Print { +public: + LiquidCrystal_I2C(uint8_t lcd_Addr,uint8_t lcd_cols,uint8_t lcd_rows); + void begin(uint8_t cols, uint8_t rows, uint8_t charsize = LCD_5x8DOTS ); + void clear(); + void home(); + void noDisplay(); + void display(); + void noBlink(); + void blink(); + void noCursor(); + void cursor(); + void scrollDisplayLeft(); + void scrollDisplayRight(); + void printLeft(); + void printRight(); + void leftToRight(); + void rightToLeft(); + void shiftIncrement(); + void shiftDecrement(); + void noBacklight(); + void backlight(); + void autoscroll(); + void noAutoscroll(); + void createChar(uint8_t, uint8_t[]); + void createChar(uint8_t location, const char *charmap); + // Example: const char bell[8] PROGMEM = {B00100,B01110,B01110,B01110,B11111,B00000,B00100,B00000}; + + void setCursor(uint8_t, uint8_t); + size_t write(uint8_t); + void command(uint8_t); + void init(); + void oled_init(); + +////compatibility API function aliases +void blink_on(); // alias for blink() +void blink_off(); // alias for noBlink() +void cursor_on(); // alias for cursor() +void cursor_off(); // alias for noCursor() +void setBacklight(uint8_t new_val); // alias for backlight() and nobacklight() +void load_custom_character(uint8_t char_num, uint8_t *rows); // alias for createChar() +void printstr(const char[]); + +////Unsupported API functions (not implemented in this library) +uint8_t status(); +void setContrast(uint8_t new_val); +uint8_t keypad(); +void setDelay(int,int); +void on(); +void off(); +uint8_t init_bargraph(uint8_t graphtype); +void draw_horizontal_graph(uint8_t row, uint8_t column, uint8_t len, uint8_t pixel_col_end); +void draw_vertical_graph(uint8_t row, uint8_t column, uint8_t len, uint8_t pixel_col_end); + + +private: + void init_priv(); + void send(uint8_t, uint8_t); + void write4bits(uint8_t); + void expanderWrite(uint8_t); + void pulseEnable(uint8_t); + uint8_t _Addr; + uint8_t _displayfunction; + uint8_t _displaycontrol; + uint8_t _displaymode; + uint8_t _numlines; + bool _oled; + uint8_t _cols; + uint8_t _rows; + uint8_t _backlightval; +}; + +#endif diff --git a/JK-BMSToPylontechCAN/LiquidCrystal_I2C.hpp b/JK-BMSToPylontechCAN/LiquidCrystal_I2C.hpp new file mode 100644 index 0000000..dab3dc9 --- /dev/null +++ b/JK-BMSToPylontechCAN/LiquidCrystal_I2C.hpp @@ -0,0 +1,344 @@ +// Based on the work by DFRobot + +#include "LiquidCrystal_I2C.h" +#include + +#include "Arduino.h" + +inline size_t LiquidCrystal_I2C::write(uint8_t value) { + send(value, Rs); + return 1; +} + +#if !defined(USE_SOFT_I2C_MASTER) && __has_include("SoftI2CMasterConfig.h") +#define USE_SOFT_I2C_MASTER +#endif + +#if defined(USE_SOFT_I2C_MASTER) +//#define USE_SOFT_I2C_MASTER_H_AS_PLAIN_INCLUDE +#include "SoftI2CMasterConfig.h" // Include configuration for sources +#include "SoftI2CMaster.h" // include sources +#elif defined(USE_SOFT_WIRE) +#define USE_SOFTWIRE_H_AS_PLAIN_INCLUDE +#include "SoftWire.h" +#endif + +// When the display powers up, it is configured as follows: +// +// 1. Display clear +// 2. Function set: +// DL = 1; 8-bit interface data +// N = 0; 1-line display +// F = 0; 5x8 dot character font +// 3. Display on/off control: +// D = 0; Display off +// C = 0; Cursor off +// B = 0; Blinking off +// 4. Entry mode set: +// I/D = 1; Increment by 1 +// S = 0; No shift +// +// Note, however, that resetting the Arduino doesn't reset the LCD, so we +// can't assume that its in that state when a sketch starts (and the +// LiquidCrystal constructor is called). + +LiquidCrystal_I2C::LiquidCrystal_I2C(uint8_t lcd_Addr, uint8_t lcd_cols, uint8_t lcd_rows) { + _Addr = lcd_Addr; + _cols = lcd_cols; + _rows = lcd_rows; + _backlightval = LCD_NOBACKLIGHT; + _oled = false; +} + +void LiquidCrystal_I2C::oled_init() { + _oled = true; + init_priv(); +} + +void LiquidCrystal_I2C::init() { + init_priv(); +} + +void LiquidCrystal_I2C::init_priv() { +#if defined(USE_SOFT_I2C_MASTER) + i2c_init(); +#else + Wire.begin(); +#endif + _displayfunction = LCD_4BITMODE | LCD_1LINE | LCD_5x8DOTS; + begin(_cols, _rows); +} + +void LiquidCrystal_I2C::begin(uint8_t cols __attribute__((unused)), uint8_t lines, uint8_t dotsize) { + if (lines > 1) { + _displayfunction |= LCD_2LINE; + } + _numlines = lines; + + // for some 1 line displays you can select a 10 pixel high font + if ((dotsize != 0) && (lines == 1)) { + _displayfunction |= LCD_5x10DOTS; + } + + // SEE PAGE 45/46 FOR INITIALIZATION SPECIFICATION! + // according to datasheet, we need at least 40ms after power rises above 2.7V + // before sending commands. Arduino can turn on way before 4.5V so we'll wait 50 + delay(50); + + // Now we pull both RS and R/W low to begin commands + expanderWrite(_backlightval); // reset expander and turn backlight off (Bit 8 =1) + delay(1000); + + //put the LCD into 4 bit mode + // this is according to the hitachi HD44780 datasheet + // figure 24, pg 46 + + // we start in 8bit mode, try to set 4 bit mode + write4bits(0x03 << 4); + delayMicroseconds(4500); // wait min 4.1ms + + // second try + write4bits(0x03 << 4); + delayMicroseconds(4500); // wait min 4.1ms + + // third go! + write4bits(0x03 << 4); + delayMicroseconds(150); + + // finally, set to 4-bit interface + write4bits(0x02 << 4); + + // set # lines, font size, etc. + command(LCD_FUNCTIONSET | _displayfunction); + + // turn the display on with no cursor or blinking default + _displaycontrol = LCD_DISPLAYON | LCD_CURSOROFF | LCD_BLINKOFF; + display(); + + // clear it off + clear(); + + // Initialize to default text direction (for roman languages) + _displaymode = LCD_ENTRYLEFT | LCD_ENTRYSHIFTDECREMENT; + + // set the entry mode + command(LCD_ENTRYMODESET | _displaymode); + + home(); + +} + +/********** high level commands, for the user! */ +void LiquidCrystal_I2C::clear() { + command(LCD_CLEARDISPLAY); // clear display, set cursor position to zero + delayMicroseconds(2000); // this command takes a long time! + if (_oled) + setCursor(0, 0); +} + +void LiquidCrystal_I2C::home() { + command(LCD_RETURNHOME); // set cursor position to zero + delayMicroseconds(2000); // this command takes a long time! +} + +void LiquidCrystal_I2C::setCursor(uint8_t col, uint8_t row) { + int row_offsets[] = { 0x00, 0x40, 0x14, 0x54 }; + if (row > _numlines) { + row = _numlines - 1; // we count rows starting w/0 + } + command(LCD_SETDDRAMADDR | (col + row_offsets[row])); +} + +// Turn the display on/off (quickly) +void LiquidCrystal_I2C::noDisplay() { + _displaycontrol &= ~LCD_DISPLAYON; + command(LCD_DISPLAYCONTROL | _displaycontrol); +} +void LiquidCrystal_I2C::display() { + _displaycontrol |= LCD_DISPLAYON; + command(LCD_DISPLAYCONTROL | _displaycontrol); +} + +// Turns the underline cursor on/off +void LiquidCrystal_I2C::noCursor() { + _displaycontrol &= ~LCD_CURSORON; + command(LCD_DISPLAYCONTROL | _displaycontrol); +} +void LiquidCrystal_I2C::cursor() { + _displaycontrol |= LCD_CURSORON; + command(LCD_DISPLAYCONTROL | _displaycontrol); +} + +// Turn on and off the blinking cursor +void LiquidCrystal_I2C::noBlink() { + _displaycontrol &= ~LCD_BLINKON; + command(LCD_DISPLAYCONTROL | _displaycontrol); +} +void LiquidCrystal_I2C::blink() { + _displaycontrol |= LCD_BLINKON; + command(LCD_DISPLAYCONTROL | _displaycontrol); +} + +// These commands scroll the display without changing the RAM +void LiquidCrystal_I2C::scrollDisplayLeft(void) { + command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVELEFT); +} +void LiquidCrystal_I2C::scrollDisplayRight(void) { + command(LCD_CURSORSHIFT | LCD_DISPLAYMOVE | LCD_MOVERIGHT); +} + +// This is for text that flows Left to Right +void LiquidCrystal_I2C::leftToRight(void) { + _displaymode |= LCD_ENTRYLEFT; + command(LCD_ENTRYMODESET | _displaymode); +} + +// This is for text that flows Right to Left +void LiquidCrystal_I2C::rightToLeft(void) { + _displaymode &= ~LCD_ENTRYLEFT; + command(LCD_ENTRYMODESET | _displaymode); +} + +// This will 'right justify' text from the cursor +void LiquidCrystal_I2C::autoscroll(void) { + _displaymode |= LCD_ENTRYSHIFTINCREMENT; + command(LCD_ENTRYMODESET | _displaymode); +} + +// This will 'left justify' text from the cursor +void LiquidCrystal_I2C::noAutoscroll(void) { + _displaymode &= ~LCD_ENTRYSHIFTINCREMENT; + command(LCD_ENTRYMODESET | _displaymode); +} + +// Allows us to fill the first 8 CGRAM locations +// with custom characters +void LiquidCrystal_I2C::createChar(uint8_t location, uint8_t charmap[]) { + location &= 0x7; // we only have 8 locations 0-7 + command(LCD_SETCGRAMADDR | (location << 3)); + for (int i = 0; i < 8; i++) { + write(charmap[i]); + } +} + +//createChar with PROGMEM input +void LiquidCrystal_I2C::createChar(uint8_t location, const char *charmap) { + location &= 0x7; // we only have 8 locations 0-7 + command(LCD_SETCGRAMADDR | (location << 3)); + for (int i = 0; i < 8; i++) { + write(pgm_read_byte_near(charmap++)); + } +} + +// Turn the (optional) backlight off/on +void LiquidCrystal_I2C::noBacklight(void) { + _backlightval = LCD_NOBACKLIGHT; + expanderWrite(0); +} + +void LiquidCrystal_I2C::backlight(void) { + _backlightval = LCD_BACKLIGHT; + expanderWrite(0); +} + +/*********** mid level commands, for sending data/cmds */ + +inline void LiquidCrystal_I2C::command(uint8_t value) { + send(value, 0); +} + +/************ low level data pushing commands **********/ + +// write either command or data +void LiquidCrystal_I2C::send(uint8_t value, uint8_t mode) { + uint8_t highnib = value & 0xf0; + uint8_t lownib = (value << 4) & 0xf0; + write4bits((highnib) | mode); + write4bits((lownib) | mode); +} + +void LiquidCrystal_I2C::write4bits(uint8_t value) { + expanderWrite(value); + pulseEnable(value); +} + +void LiquidCrystal_I2C::expanderWrite(uint8_t _data) { +#if defined(USE_SOFT_I2C_MASTER) + i2c_write_byte(_Addr << 1, _data | _backlightval); +#else + Wire.beginTransmission(_Addr); + Wire.write((int )(_data) | _backlightval); + Wire.endTransmission(); +#endif +} + +void LiquidCrystal_I2C::pulseEnable(uint8_t _data) { + expanderWrite(_data | En); // En high + delayMicroseconds(1); // enable pulse must be >450ns + + expanderWrite(_data & ~En); // En low + delayMicroseconds(50); // commands need > 37us to settle +} + +// Alias functions + +void LiquidCrystal_I2C::cursor_on() { + cursor(); +} + +void LiquidCrystal_I2C::cursor_off() { + noCursor(); +} + +void LiquidCrystal_I2C::blink_on() { + blink(); +} + +void LiquidCrystal_I2C::blink_off() { + noBlink(); +} + +void LiquidCrystal_I2C::load_custom_character(uint8_t char_num, uint8_t *rows) { + createChar(char_num, rows); +} + +void LiquidCrystal_I2C::setBacklight(uint8_t new_val) { + if (new_val) { + backlight(); // turn backlight on + } else { + noBacklight(); // turn backlight off + } +} + +void LiquidCrystal_I2C::printstr(const char c[]) { + //This function is not identical to the function used for "real" I2C displays + //it's here so the user sketch doesn't have to be changed + print(c); +} + +// unsupported API functions +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +void LiquidCrystal_I2C::off() { +} +void LiquidCrystal_I2C::on() { +} +void LiquidCrystal_I2C::setDelay(int cmdDelay, int charDelay) { +} +uint8_t LiquidCrystal_I2C::status() { + return 0; +} +uint8_t LiquidCrystal_I2C::keypad() { + return 0; +} +uint8_t LiquidCrystal_I2C::init_bargraph(uint8_t graphtype) { + return 0; +} +void LiquidCrystal_I2C::draw_horizontal_graph(uint8_t row, uint8_t column, uint8_t len, uint8_t pixel_col_end) { +} +void LiquidCrystal_I2C::draw_vertical_graph(uint8_t row, uint8_t column, uint8_t len, uint8_t pixel_row_end) { +} +void LiquidCrystal_I2C::setContrast(uint8_t new_val) { +} +#pragma GCC diagnostic pop + diff --git a/JK-BMSToPylontechCAN/LongUnion.h b/JK-BMSToPylontechCAN/LongUnion.h new file mode 100644 index 0000000..1e09695 --- /dev/null +++ b/JK-BMSToPylontechCAN/LongUnion.h @@ -0,0 +1,146 @@ +/* + * LongUnion.h + * + * Copyright (C) 2020-2022 Armin Joachimsmeyer + * Email: armin.joachimsmeyer@gmail.com + * + * This file is part of Arduino-Utils https://github.com/ArminJo/Arduino-Utils. + * + * Arduino-Utils 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 . + * + */ + +#if !defined(_WORD_UNION_H) || !defined(_LONG_UNION_H) || !defined(_LONG_LONG_UNION_H) + +#include + +#ifndef _WORD_UNION_H +#define _WORD_UNION_H +/** + * Union to specify parts / manifestations of a 16 bit Word without casts and shifts. + * It also supports the compiler generating small code. + * Usage: WordUnion tWord; + * tWord.UByte.HighByte = 0x12; + */ +union WordUnion { + struct { + uint8_t LowByte; + uint8_t HighByte; + } UByte; + struct { + int8_t LowByte; + int8_t HighByte; + } Byte; + uint8_t UBytes[2]; // UBytes[0] is LowByte + int8_t Bytes[2]; + uint16_t UWord; + int16_t Word; + uint8_t *BytePointer; +}; +#endif // _WORD_UNION_H + +#ifndef _LONG_UNION_H +#define _LONG_UNION_H +/** + * Union to specify parts / manifestations of a 32 bit Long without casts and shifts. + * It also supports the compiler generating small code. + */ +union LongUnion { + struct { + uint8_t LowByte; + uint8_t MidLowByte; + uint8_t MidHighByte; + uint8_t HighByte; + } UByte; + struct { + int8_t LowByte; + int8_t MidLowByte; + int8_t MidHighByte; + int8_t HighByte; + } Byte; + /* Does not work for STM32 + struct { + uint8_t LowByte; + uint16_t MidWord; + uint8_t HighByte; + } UByteWord; + */ + struct { + uint16_t LowWord; + uint16_t HighWord; + } UWord; + struct { + int16_t LowWord; + int16_t HighWord; + } Word; + struct { + WordUnion LowWord; + WordUnion HighWord; + } WordUnion; + 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]; + int16_t Words[2]; + uint32_t ULong; + int32_t Long; +}; +#endif // _LONG_UNION_H + +#ifndef _LONG_LONG_UNION_H +#define _LONG_LONG_UNION_H +/** + * Union to specify parts / manifestations of a 64 bit LongLong without casts and shifts. + * It also supports the compiler generating small code. + */ +union LongLongUnion { + struct { + uint16_t LowWord; + uint16_t MidLowWord; + uint16_t MidHighWord; + uint16_t HighWord; + } UWord; + struct { + int16_t LowWord; + int16_t MidLowWord; + int16_t MidHighWord; + int16_t HighWord; + } Word; + struct { + WordUnion LowWord; + WordUnion MidLowWord; + WordUnion MidHighWord; + WordUnion HighWord; + } WordUnion; + struct { + uint32_t LowLong; + uint32_t HighLong; + } ULong; + struct { + int32_t LowLong; + int32_t HighLong; + } Long; + struct { + LongUnion LowLong; + LongUnion HighLong; + } LongUnion; + uint8_t UBytes[8]; // seems to have the same code size as using struct UByte + int8_t Bytes[8]; + uint16_t UWords[4]; + int16_t Words[4]; + uint64_t ULongLong; + int64_t LongLong; +}; +#endif // _LONG_LONG_UNION_H + +#endif // !defined(_WORD_UNION_H) || !defined(_LONG_UNION_H) || !defined(_LONG_LONG_UNION_H) diff --git a/JK-BMSToPylontechCAN/MCP2515_TX.h b/JK-BMSToPylontechCAN/MCP2515_TX.h new file mode 100644 index 0000000..be1652a --- /dev/null +++ b/JK-BMSToPylontechCAN/MCP2515_TX.h @@ -0,0 +1,32 @@ +/* + * MCP2515_TX.h + * + * + * Copyright (C) 2023 Armin Joachimsmeyer + * Email: armin.joachimsmeyer@gmail.com + * + * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * + * Arduino-Utils 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 _MCP2515_TX_H +#define _MCP2515_TX_H + +#include + +bool initializeCAN(); // Return true if error happens +bool sendCANMessage(uint16_t aCANId, uint8_t aLengthOfBuffer, const uint8_t *aSendDataBufferPointer); // Return true if error happens +#endif // _MCP2515_TX_H diff --git a/JK-BMSToPylontechCAN/MCP2515_TX.hpp b/JK-BMSToPylontechCAN/MCP2515_TX.hpp new file mode 100644 index 0000000..9455dae --- /dev/null +++ b/JK-BMSToPylontechCAN/MCP2515_TX.hpp @@ -0,0 +1,161 @@ +/* + * MCP2515_TX.hpp + * + * Functions to control send only functions for MCP2515 CAN controller + * + * + * Copyright (C) 2023 Armin Joachimsmeyer + * Email: armin.joachimsmeyer@gmail.com + * + * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * + * Arduino-Utils 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 _MCP2515_TX_HPP +#define _MCP2515_TX_HPP + +#include "mcp2515_can_dfs.h" + +#include +SPISettings sSPISettings(4000000, MSBFIRST, SPI_MODE0); +#if !defined SPI_CS_PIN +#define SPI_CS_PIN 9 // Pin 9 seems to be the default pin for the Arduino CAN bus shield. Alternately you can use pin 10 on this shield +#endif + +#define MCP2515_RETURN_OK false + +#define MCP2515_CAN_CONTROL_REGISTER_CONTENT MODE_NORMAL // default mode +//#define MCP2515_CAN_CONTROL_REGISTER_CONTENT MODE_ONESHOT | CLKOUT_ENABLE; // Alternative mode with no resending and clock output at pin 3 + +void resetMCP2515(void) { + SPI.beginTransaction(sSPISettings); + digitalWrite(SPI_CS_PIN, LOW); + SPI.transfer(0xc0); + digitalWrite(SPI_CS_PIN, HIGH); + SPI.endTransaction(); + delayMicroseconds(10); +} + +uint8_t readMCP2515Register(uint8_t address) { + uint8_t value; + + SPI.beginTransaction(sSPISettings); + digitalWrite(SPI_CS_PIN, LOW); + SPI.transfer(0x03); + SPI.transfer(address); + value = SPI.transfer(0x00); + digitalWrite(SPI_CS_PIN, HIGH); + SPI.endTransaction(); + + return value; +} + +void modifyMCP2515Register(uint8_t address, uint8_t mask, uint8_t value) { + SPI.beginTransaction(sSPISettings); + digitalWrite(SPI_CS_PIN, LOW); + SPI.transfer(0x05); + SPI.transfer(address); + SPI.transfer(mask); + SPI.transfer(value); + digitalWrite(SPI_CS_PIN, HIGH); + SPI.endTransaction(); +} + +void writeMCP2515Register(uint8_t address, uint8_t value) { + SPI.beginTransaction(sSPISettings); + digitalWrite(SPI_CS_PIN, LOW); + SPI.transfer(0x02); + SPI.transfer(address); + SPI.transfer(value); + digitalWrite(SPI_CS_PIN, HIGH); + SPI.endTransaction(); +} + +/* + * return true if error happens + */ +bool initializeCAN() { + pinMode(SPI_CS_PIN, OUTPUT); + + SPI.begin(); // start SPI + + resetMCP2515(); // Reset MCP2515 + + // Set Configuration mode + writeMCP2515Register(MCP_CANCTRL, MODE_CONFIG); + if (readMCP2515Register(MCP_CANCTRL) != MODE_CONFIG) { + return true; + } + + /* + * Set timing for 16 MHz crystal and 500 kBit/s + */ +#if defined(CRYSTAL_20MHZ_ASSEMBLED) + writeMCP2515Register(MCP_CNF1, MCP_20MHz_500kBPS_CFG1); // Baud Rate Prescaler + writeMCP2515Register(MCP_CNF2, MCP_20MHz_500kBPS_CFG2); // 0x80 is BTLMODE and always set + writeMCP2515Register(MCP_CNF3, MCP_20MHz_500kBPS_CFG3); +#else + writeMCP2515Register(MCP_CNF1, MCP_16MHz_500kBPS_CFG1); // Baud Rate Prescaler + writeMCP2515Register(MCP_CNF2, MCP_16MHz_500kBPS_CFG2); // 0x80 is BTLMODE and always set + writeMCP2515Register(MCP_CNF3, MCP_16MHz_500kBPS_CFG3); +#endif + + // Reset Configuration mode + writeMCP2515Register(MCP_CANCTRL, MCP2515_CAN_CONTROL_REGISTER_CONTENT); + if (readMCP2515Register(MCP_CANCTRL) != MCP2515_CAN_CONTROL_REGISTER_CONTENT) { + return true; + } + + return false; +} + +/* + * return true if error happens + */ +bool sendCANMessage(uint16_t aCANId, uint8_t aLengthOfBuffer, const uint8_t *aSendDataBufferPointer) { + + /* + * We use transmit buffer 0 + */ + writeMCP2515Register(MCP_TXB0SIDH, aCANId >> 3); // write bit 3:10 of ID + writeMCP2515Register(MCP_TXB0SIDL, aCANId << 5); // write bit 0:2 and flag "no extended" + writeMCP2515Register(MCP_TXB0DLC, aLengthOfBuffer); + + // Fill buffer + for (uint_fast8_t i = 0; i < aLengthOfBuffer; i++) { + writeMCP2515Register(MCP_TXB0D0 + i, aSendDataBufferPointer[i]); + } + + writeMCP2515Register(MCP_TXB0CTRL, MCP_TXB_TXREQ_M); + + /* + * Check for end of transmission, and if an error happened + */ + while (readMCP2515Register(MCP_TXB0CTRL) & MCP_TXB_TXREQ_M) { + if (readMCP2515Register(MCP_TXB0CTRL) & (MCP_TXB_TXERR_M | MCP_TXB_MLOA_M | MCP_TXB_ABTF_M)) { + /* + * Error happened here, abort transfer. First retransmit is still pending! + */ + writeMCP2515Register(MCP_CANCTRL, ABORT_TX | MCP2515_CAN_CONTROL_REGISTER_CONTENT); // Set "Abort All Pending Transmissions" bit + delayMicroseconds(10); + writeMCP2515Register(MCP_CANCTRL, MCP2515_CAN_CONTROL_REGISTER_CONTENT); // Reset "Abort All Pending Transmissions" bit + return true; // Error + } + } + + return false; +} +#endif // _MCP2515_TX_HPP diff --git a/JK-BMSToPylontechCAN/Pylontech_CAN.cpp b/JK-BMSToPylontechCAN/Pylontech_CAN.cpp new file mode 100644 index 0000000..efbca90 --- /dev/null +++ b/JK-BMSToPylontechCAN/Pylontech_CAN.cpp @@ -0,0 +1,102 @@ +/* + * Pylontech_CAN.cpp + * + * Functions to fill and send CAN data defined in Pylontech_CAN.h + * + * Useful links: + * https://www.setfirelabs.com/green-energy/pylontech-can-reading-can-replication + * https://www.skpang.co.uk/products/teensy-4-1-triple-can-board-with-240x240-ips-lcd + * + * Copyright (C) 2023 Armin Joachimsmeyer + * Email: armin.joachimsmeyer@gmail.com + * + * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * + * Arduino-Utils 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 _PYLONTECH_CAN_CPP +#define _PYLONTECH_CAN_CPP + +#include + +#if !defined(LOCAL_DEBUG) +//#define LOCAL_DEBUG +#endif + +#include "MCP2515_TX.h" // my reduced driver +#include "Pylontech_CAN.h" + +struct PylontechCANBatteryLimitsFrameStruct PylontechCANBatteryLimitsFrame; +struct PylontechCANSohSocFrameStruct PylontechCANSohSocFrame; +struct PylontechCANCurrentValuesFrameStruct PylontechCANCurrentValuesFrame; +struct PylontechCANManufacturerFrameStruct PylontechCANManufacturerFrame; +struct PylontechCANBatteryRequesFrameStruct PylontechCANBatteryRequesFrame; +struct PylontechCANAliveFrameStruct PylontechCANAliveFrameStruct; +struct PylontechCANErrorsWarningsFrameStruct PylontechCANErrorsWarningsFrame; + +void fillAllCANData(struct JKReplyStruct *aJKFAllReply) { + PylontechCANBatteryLimitsFrame.fillFrame(aJKFAllReply); + PylontechCANSohSocFrame.fillFrame(aJKFAllReply); + PylontechCANBatteryRequesFrame.fillFrame(aJKFAllReply); + PylontechCANErrorsWarningsFrame.fillFrame(aJKFAllReply); + PylontechCANCurrentValuesFrame.fillFrame(aJKFAllReply); +} + +void sendPylontechCANFrame(struct PylontechCANFrameStruct *aPylontechCANFrame) { + sendCANMessage(aPylontechCANFrame->PylontechCANFrameInfo.CANId, aPylontechCANFrame->PylontechCANFrameInfo.FrameLength, + aPylontechCANFrame->FrameData.UBytes); +} + +void printPylontechCANFrame(struct PylontechCANFrameStruct *aPylontechCANFrame) { + Serial.print(F("CANId=0x")); + Serial.print(aPylontechCANFrame->PylontechCANFrameInfo.CANId, HEX); + Serial.print(F(", FrameLength=")); + Serial.print(aPylontechCANFrame->PylontechCANFrameInfo.FrameLength); + Serial.print(F(", Data=0x")); + for (uint_fast8_t i = 0; i < aPylontechCANFrame->PylontechCANFrameInfo.FrameLength; ++i) { + if (i != 0) { + Serial.print(F(", 0x")); + } + Serial.print(aPylontechCANFrame->FrameData.UBytes[i], HEX); + } + Serial.println(); +} + +/* + * Inverter reply every second: 0x305: 00-00-00-00-00-00-00-00 + * If no CAN receiver is attached, every frame is retransmitted once, because of the NACK error. + * Or use CAN.writeRegister(REG_CANCTRL, 0x08); // One Shot Mode + */ +void sendPylontechAllCANFrames(bool aDebugModeActive) { + if (aDebugModeActive) { + printPylontechCANFrame(reinterpret_cast(&PylontechCANBatteryLimitsFrame)); + printPylontechCANFrame(reinterpret_cast(&PylontechCANSohSocFrame)); + printPylontechCANFrame(reinterpret_cast(&PylontechCANCurrentValuesFrame)); + printPylontechCANFrame(reinterpret_cast(&PylontechCANManufacturerFrame)); + printPylontechCANFrame(reinterpret_cast(&PylontechCANBatteryRequesFrame)); + printPylontechCANFrame(reinterpret_cast(&PylontechCANAliveFrameStruct)); + printPylontechCANFrame(reinterpret_cast(&PylontechCANErrorsWarningsFrame)); + } + sendPylontechCANFrame(reinterpret_cast(&PylontechCANBatteryLimitsFrame)); + sendPylontechCANFrame(reinterpret_cast(&PylontechCANSohSocFrame)); + sendPylontechCANFrame(reinterpret_cast(&PylontechCANCurrentValuesFrame)); + sendPylontechCANFrame(reinterpret_cast(&PylontechCANManufacturerFrame)); + sendPylontechCANFrame(reinterpret_cast(&PylontechCANBatteryRequesFrame)); + sendPylontechCANFrame(reinterpret_cast(&PylontechCANAliveFrameStruct)); + sendPylontechCANFrame(reinterpret_cast(&PylontechCANErrorsWarningsFrame)); +} + +#endif // _PYLONTECH_CAN_H diff --git a/JK-BMSToPylontechCAN/Pylontech_CAN.h b/JK-BMSToPylontechCAN/Pylontech_CAN.h new file mode 100644 index 0000000..f110ef1 --- /dev/null +++ b/JK-BMSToPylontechCAN/Pylontech_CAN.h @@ -0,0 +1,269 @@ +/* + * Pylontech_CAN.h + * + * Definitions for the CAN frames to send as Pylon protocol. + * TODO The generated output does not correspond to the logs below and + * published at https://www.setfirelabs.com/green-energy/pylontech-can-reading-can-replication + * + * Copyright (C) 2023 Armin Joachimsmeyer + * Email: armin.joachimsmeyer@gmail.com + * + * This file is part of ArduinoUtils https://github.com/ArminJo/PVUtils. + * + * Arduino-Utils 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 _PYLONTECH_CAN_H +#define _PYLONTECH_CAN_H + +#include +#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 + */ +#define PYLON_CAN_NETWORK_ALIVE_MSG_FRAME_ID 0x305 +#define PYLON_CAN_BATTERY_MANUFACTURER_FRAME_ID 0x35E // Manufacturer name ("PYLON") +#define PYLON_CAN_BATTERY_CHARGE_REQUEST_FRAME_ID 0x35C // Battery charge request flags +#define PYLON_CAN_BATTERY_CURRENT_VALUES_U_I_T_FRAME_ID 0x356 // Voltage / Current / Temperature +#define PYLON_CAN_BATTERY_SOC_SOH_FRAME_ID 0x355 // State of Health (SOH) / State of Charge (SOC) +#define PYLON_CAN_BATTERY_LIMITS_FRAME_ID 0x351 // Battery voltage + current limits +#define PYLON_CAN_BATTERY_ERROR_WARNINGS_FRAME_ID 0x359 // Protection & Alarm flags + +extern struct PylontechCANBatteryLimitsFrameStruct PylontechCANBatteryLimitsFrame; +extern struct PylontechCANSohSocFrameStruct PylontechCANSohSocFrame; +extern struct PylontechCANCurrentValuesFrameStruct PylontechCANCurrentValuesFrame; +extern struct PylontechCANManufacturerFrameStruct PylontechCANManufacturerFrame; +extern struct PylontechCANBatteryRequesFrameStruct PylontechCANBatteryRequesFrame; +extern struct PylontechCANAliveFrameStruct PylontechCANAliveFrameStruct; +extern struct PylontechCANErrorsWarningsFrameStruct PylontechCANErrorsWarningsFrame; + +void fillPylontechCANBatteryLimitsFrame(struct JKReplyStruct *aJKFAllReply); +void fillPylontechCANBatterySohSocFrame(struct JKReplyStruct *aJKFAllReply); +void fillPylontechCANManufactureFrame(struct JKReplyStruct *aJKFAllReply); +void fillPylontechCANBatteryRequesFrame(struct JKReplyStruct *aJKFAllReply); +void fillPylontechCANErrors_WarningsFrame(struct JKReplyStruct *aJKFAllReply); +void fillPylontechCANCurrentValuesFrame(struct JKReplyStruct *aJKFAllReply); + +void fillAllCANData(struct JKReplyStruct *aJKFAllReply); +void sendPylontechAllCANFrames(bool aDebugModeActive); + +struct PylontechCANFrameInfoStruct { + // Both values will be statically initialized in each instance + uint16_t CANId; + uint8_t FrameLength; +}; + +struct PylontechCANFrameStruct { + struct PylontechCANFrameInfoStruct PylontechCANFrameInfo; + LongLongUnion FrameData; +}; + +struct PylontechCANBatteryLimitsFrameStruct { + struct PylontechCANFrameInfoStruct PylontechCANFrameInfo = { PYLON_CAN_BATTERY_LIMITS_FRAME_ID, 8 }; + 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 + } FrameData; + void fillFrame(struct JKReplyStruct *aJKFAllReply) { + FrameData.BatteryChargeOvervoltage100Millivolt = swap(aJKFAllReply->BatteryOvervoltageProtection10Millivolt) / 10; + FrameData.BatteryChargeCurrentLimit100Milliampere = swap(aJKFAllReply->ChargeOvercurrentProtectionAmpere) * 10; + FrameData.BatteryDischargeCurrentLimit100Milliampere = swap(aJKFAllReply->DischargeOvercurrentProtectionAmpere) * 10; + FrameData.BatteryDischarge100Millivolt = swap(aJKFAllReply->BatteryUndervoltageProtection10Millivolt) / 10; + } +}; + +struct PylontechCANSohSocFrameStruct { + struct PylontechCANFrameInfoStruct PylontechCANFrameInfo = { PYLON_CAN_BATTERY_SOC_SOH_FRAME_ID, 4 }; + struct { + uint16_t SOCPercent; + uint16_t SOHPercent = 95; // fixed 95 + } FrameData; + void fillFrame(struct JKReplyStruct *aJKFAllReply) { + FrameData.SOCPercent = aJKFAllReply->SOCPercent; + } +}; + +struct PylontechCANManufacturerFrameStruct { + struct PylontechCANFrameInfoStruct PylontechCANFrameInfo = { PYLON_CAN_BATTERY_MANUFACTURER_FRAME_ID, 8 }; + struct { + char ManufacturerName[8] = { 'P', 'Y', 'L', 'O', 'N', ' ', ' ', ' ' }; + } FrameData; +}; + +/* + * ForceChargeRequestI / bit 5 is designed for inverter allows battery to shut down, and able to wake battery up to charge it. + * ForceChargeRequestII / bit 4 is designed for inverter doesn`t want battery to shut down, able to charge battery before shut down to avoid low energy. + * 2 bytes + */ +struct PylontechCANBatteryRequesFrameStruct { + struct PylontechCANFrameInfoStruct PylontechCANFrameInfo = { PYLON_CAN_BATTERY_CHARGE_REQUEST_FRAME_ID, 2 }; + struct { + bool :3; // unused + // 0=off 1=Request + /* + * From Pylontech manual: Depending on the soc level, there will be a regularly (3 month) fully charge requesting during continuous operation as well. + * It will be handled automatically by the communication between BESS and external device. + * This is to stop SOC calculations drifting too far from reality when the battery has not had a full charge for n days. + */ + bool FullChargeRequest :1; + // Force to charge battery even from the grid. + // From Battery-Communications-Integration-Guide-V2.5-1.pdf: Command sent by the BMS telling the inverter to charge the battery from any available power source regardless of inverter settings. + bool ForceChargeRequestII :1; + bool ForceChargeRequestI :1; + bool DischargeEnable :1; + bool ChargeEnable :1; + uint8_t Filler = 0; + } FrameData; + void fillFrame(struct JKReplyStruct *aJKFAllReply) { + FrameData.DischargeEnable = aJKFAllReply->StatusUnion.StatusBits.ChargeMosFetActive; + FrameData.ChargeEnable = aJKFAllReply->StatusUnion.StatusBits.DischargeMosFetActive; + + // I do not know the semantics of ForceChargeRequest flags so it is only a guess here + if (aJKFAllReply->SOCPercent < 20) { + FrameData.ForceChargeRequestI = 1; + } + // If battery drops below lower voltage. See https://powerforum.co.za/topic/13587-battery-anomaly-on-synsynk-hybrid-inverter/ + if (swap(aJKFAllReply->Battery10Millivolt) < swap(aJKFAllReply->BatteryUndervoltageProtection10Millivolt)) { + FrameData.ForceChargeRequestII = 1; + } + } +}; + +struct PylontechCANAliveFrameStruct { + struct PylontechCANFrameInfoStruct PylontechCANFrameInfo = { PYLON_CAN_NETWORK_ALIVE_MSG_FRAME_ID, 8 }; + struct { + uint8_t AlivePacketArray[8] = { 33 }; + } FrameData; +}; + +struct PylontechCANErrorsWarningsFrameStruct { + struct PylontechCANFrameInfoStruct PylontechCANFrameInfo = { PYLON_CAN_BATTERY_ERROR_WARNINGS_FRAME_ID, 7 }; + struct { + // 0=off 1=on + // Byte 0 + bool :1; // unused + bool CellOvervoltageError :1; // 0x02 + bool CellUndervoltageError :1; // 0x04 + bool CellOvertemperatureError :1; // 0x08 + bool CellUndertemperatureError :1; // 0x10 + bool :2; // unused + bool DischargeOvercurrentError :1; // 0x80 + + // Byte 1 +// bool :0; + bool ChargeOvercurrentError :1; // 0x01 + bool :6; // unused + bool SystemError :1; // 0x80 + + // Byte 2 +// bool :0; + bool :1; // unused + bool CellHighVoltageWarning :1; // 0x02 + bool CellLowVoltageWarning :1; // 0x04 + bool CellHighTemperatureWarning :1; // 0x08 + bool CellLowTemperatureWarning :1; // 0x10 + bool :2; // unused + bool DischargeHighCurrentWarning :1; // 0x80 + + // Byte 3 +// bool :0; + bool ChargeHighCurrentWarning :1; // 0x01 + // found in documentation +// bool :2; // unused +// bool InternalCommunicationFail :1; +// bool :4; // unused + + // Found in software + bool :6; // unused + bool SystemWarning :1; // 0x80 + + // Byte 4 to 6 +// bool :0; + uint8_t ModuleNumber = 1; // 0 to 255 + uint8_t Token1 = 0x50; // 'P' + uint8_t Token2 = 0x4E; // 'N' + } FrameData; + void fillFrame(struct JKReplyStruct *aJKFAllReply) { + /* + * 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; + + // Byte 1 + FrameData.ChargeOvercurrentError = aJKFAllReply->AlarmUnion.AlarmBits.ChargeOvercurrentAlarm; + FrameData.SystemError = aJKFAllReply->StatusUnion.StatusBits.BatteryDown; +// if (aJKFAllReply->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; + // 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; + + // Byte 3 + // Use the same values as for error here + FrameData.ChargeHighCurrentWarning = aJKFAllReply->AlarmUnion.AlarmBits.ChargeOvercurrentAlarm; + FrameData.SystemError = aJKFAllReply->StatusUnion.StatusBits.BatteryDown; +// if (aJKFAllReply->SOCPercent < 10) { +// FrameData.SystemWarning = 1; +// } + + } +}; + +struct PylontechCANCurrentValuesFrameStruct { + struct PylontechCANFrameInfoStruct PylontechCANFrameInfo = { PYLON_CAN_BATTERY_CURRENT_VALUES_U_I_T_FRAME_ID, 6 }; + struct { + int16_t Voltage100Millivolt; // 0 to 32767 + int16_t Current100Milliampere; // -2500 to 2500 + int16_t Temperature100Millicelsius; // -500 to 750 + } FrameData; + void fillFrame(struct JKReplyStruct *aJKFAllReply) { + (void) aJKFAllReply; // To avoid [-Wunused-parameter] warning + FrameData.Voltage100Millivolt = JKComputedData.BatteryVoltage10Millivolt / 10; + FrameData.Current100Milliampere = JKComputedData.Battery10MilliAmpere / 10; + FrameData.Temperature100Millicelsius = JKComputedData.TemperatureMaximum * 10; + } +}; + +#endif // _PYLONTECH_CAN_H diff --git a/JK-BMSToPylontechCAN/SoftI2CMaster.h b/JK-BMSToPylontechCAN/SoftI2CMaster.h new file mode 100644 index 0000000..528c06e --- /dev/null +++ b/JK-BMSToPylontechCAN/SoftI2CMaster.h @@ -0,0 +1,1160 @@ +/* Arduino SoftI2C library. + * SoftI2CMaster.h + * + * Version 2.1.8 + * + * Copyright (C) 2013-2021, Bernhard Nebel and Peter Fleury + * + * This is a very fast and very light-weight software I2C-master library + * written in assembler. It is based on Peter Fleury's I2C software + * library: http://homepage.hispeed.ch/peterfleury/avr-software.html + * Recently, the hardware implementation has been added to the code, + * which can be enabled by defining I2C_HARDWARE. + * + * This file is part of SoftI2CMaster https://github.com/felias-fogg/SoftI2CMaster. + * + * This Library 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 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. + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Arduino I2cMaster Library. If not, see + * . + */ + +/* In order to use the library, you need to define SDA_PIN, SCL_PIN, + * SDA_PORT and SCL_PORT before including this file. Have a look at + * http://www.arduino.cc/en/Reference/PortManipulation for finding out + * which values to use. For example, if you use digital pin 3 (corresponding + * to PD3) for SDA and digital pin 13 (corresponding to PB5) + * for SCL on a standard Arduino, + * you have to use the following definitions: + * #define SDA_PIN 3 + * #define SDA_PORT PORTD + * #define SCL_PIN 5 + * #define SCL_PORT PORTB + * + * Alternatively, you can define the compile time constant I2C_HARDWARE, + * in which case the TWI hardware is used. In this case you have to use + * the standard SDA/SCL pins (and, of course, the chip needs to support + * this). + * + * You can also define the following constants (see also below): + ' - I2C_PULLUP = 1 meaning that internal pullups should be used + * - I2C_CPUFREQ, when changing CPU clock frequency dynamically + * - I2C_FASTMODE = 1 meaning that the I2C bus allows speeds up to 400 kHz + * - I2C_SLOWMODE = 1 meaning that the I2C bus will allow only up to 25 kHz + * - I2C_NOINTERRUPT = 1 in order to prohibit interrupts while + * communicating (see below). This can be useful if you use the library + * for communicating with SMbus devices, which have timeouts. + * Note, however, that interrupts are disabled from issuing a start condition + * until issuing a stop condition. So use this option with care! + * - I2C_TIMEOUT = 0...10000 msec in order to return from the I2C functions + * in case of a I2C bus lockup (i.e., SCL constantly low). 0 means no timeout. + * - I2C_MAXWAIT = 0...32767 number of retries in i2c_start_wait. 0 means never stop. + */ + +/* Changelog: + * * Version 2.1.8 + * - ArminJo: Included MACRO USE_SOFT_I2C_MASTER_H_AS_PLAIN_INCLUDE + * Version 2.1.7 + * - ArminJo: replaced all calls and jmps by rcalls and rjmps for CPUs not having call and jmp + * Version 2.1.6 + * - adapted SlowSoftWire to make it comparable to SlowSoftI2C (and a few other minor things) + * Version 2.1.5 + * - replaced all rcalls and rjmps by calls and jmps + * Version 2.1.4 + * - fixed bug in Softwire concerning repeated starts + * Version 2.1.3 + * - removed WireEmu and fixed bug in Eeprom24AA1025SoftI2C.ino + * Version 2.1 + * - added conditional to check whether it is the right MCU type + * Version 2.0 + * - added hardware support as well. + * Version 1.4: + * - added "maximum retry" in i2c_start_wait in order to avoid lockup + * - added "internal pullups", but be careful since the option stretches the I2C specs + * Version 1.3: + * - added "__attribute__ ((used))" for all functions declared with "__attribute__ ((noinline))" + * Now the module is also usable in Arduino 1.6.11+ + * Version 1.2: + * - added pragma to avoid "unused parameter warnings" (suggestion by Walter) + * - replaced wrong license file + * Version 1.1: + * - removed I2C_CLOCK_STRETCHING + * - added I2C_TIMEOUT time in msec (0..10000) until timeout or 0 if no timeout + * - changed i2c_init to return true if and only if both SDA and SCL are high + * - changed interrupt disabling so that the previous IRQ state is restored + * Version 1.0: basic functionality + */ + +#ifndef __AVR_ARCH__ +#error "Not an AVR MCU! Use 'SlowSoftI2CMaster' library instead of 'SoftI2CMaster'!" +#else +/* + * No Guard here! + * If we include it with USE_SOFT_I2C_MASTER_H_AS_PLAIN_INCLUDE defined, + * we must allow, that it can be included again without USE_SOFT_I2C_MASTER_H_AS_PLAIN_INCLUDE defined + */ +#include +#include +#include + +// Init function. Needs to be called once in the beginning. +// Returns false if SDA or SCL are low, which probably means +// a I2C bus lockup or that the lines are not pulled up. +bool __attribute__ ((noinline)) i2c_init(void) __attribute__ ((used)); + +// Start transfer function: is the 8-bit I2C address (including the R/W +// bit). +// Return: true if the slave replies with an "acknowledge", false otherwise +bool __attribute__ ((noinline)) i2c_start(uint8_t addr) __attribute__ ((used)); + +// Similar to start function, but wait for an ACK! Will timeout if I2C_MAXWAIT > 0. +bool __attribute__ ((noinline)) i2c_start_wait(uint8_t addr) __attribute__ ((used)); + +// Repeated start function: After having claimed the bus with a start condition, +// you can address another or the same chip again without an intervening +// stop condition. +// Return: true if the slave replies with an "acknowledge", false otherwise +bool __attribute__ ((noinline)) i2c_rep_start(uint8_t addr) __attribute__ ((used)); + +// Issue a stop condition, freeing the bus. +void __attribute__ ((noinline)) i2c_stop(void) asm("ass_i2c_stop") __attribute__ ((used)); + +// Write one byte to the slave chip that had been addressed +// by the previous start call. is the byte to be sent. +// Return: true if the slave replies with an "acknowledge", false otherwise +bool __attribute__ ((noinline)) i2c_write(uint8_t value) asm("ass_i2c_write") __attribute__ ((used)); + +// Read one byte. If is true, we send a NAK after having received +// the byte in order to terminate the read sequence. +uint8_t __attribute__ ((noinline)) i2c_read(bool last) __attribute__ ((used)); + +/* + * Convenience functions + */ +void i2c_write_byte(uint8_t addr, uint8_t byte); + +void i2c_read_buffer_from_register(uint8_t addr, uint8_t register_number, uint8_t * byte_buffer, uint8_t number_of_bytes_to_read); +void i2c_write_buffer_to_register(uint8_t addr, uint8_t register_number, uint8_t * byte_buffer, uint8_t number_of_bytes_to_write) ; + +void i2c_read_buffer_from_16bit_register(uint8_t addr, uint16_t register_number, uint8_t * byte_buffer, uint8_t number_of_bytes_to_read); +void i2c_write_buffer_to_16bit_register(uint8_t addr, uint16_t register_number, uint8_t * byte_buffer, uint8_t number_of_bytes_to_write); + +// Read word from register, low byte first +uint16_t i2c_read_word_from_register(uint8_t addr, uint8_t register_number); + +// Read word from register, high byte first +uint16_t i2c_read_word_swapped_from_register(uint8_t addr, uint8_t register_number); + +void i2c_write_byte_to_register(uint8_t addr, uint8_t register_number, uint8_t byte); + +// constants for reading & writing +#define I2C_READ 1 +#define I2C_WRITE 0 + +#if !defined(USE_SOFT_I2C_MASTER_H_AS_PLAIN_INCLUDE) +#ifndef _SOFTI2C_H +#define _SOFTI2C_H 1 +/* + * The implementation part of the header only library starts here + */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" + +// If you want to use the TWI hardware, you have to define I2C_HARDWARE to be 1 +#if !defined(I2C_HARDWARE) +#define I2C_HARDWARE 0 +#endif + +#if I2C_HARDWARE +#if !defined(TWDR) +#error This chip does not support hardware I2C. Please undefine I2C_HARDWARE +#endif +#endif + +// You can set I2C_CPUFREQ independently of F_CPU if you +// change the CPU frequency on the fly. If you do not define it, +// it will use the value of F_CPU +#if !defined(I2C_CPUFREQ) +#define I2C_CPUFREQ F_CPU +#endif + +// If I2C_FASTMODE is set to 1, then the highest possible frequency below 400kHz +// is selected. Be aware that not all slave chips may be able to deal with that! +#if !defined(I2C_FASTMODE) +#define I2C_FASTMODE 0 +#endif + +// If I2C_FASTMODE is not defined or defined to be 0, then you can set +// I2C_SLOWMODE to 1. In this case, the I2C frequency will not be higher +// than 25KHz. This could be useful for problematic buses with high pull-ups +// and high capacitance. +#if !defined(I2C_SLOWMODE) +#define I2C_SLOWMODE 0 +#endif + +// If I2C_PULLUP is set to 1, then the internal pull-up resistors are used. +// This does not conform with the I2C specs, since the bus lines will be +// temporarily in high-state and the internal resistors have roughly 50k. +// With low bus speeds und short buses it usually works, though (hopefully). +#if !defined(I2C_PULLUP) +#define I2C_PULLUP 0 +#endif + +// if I2C_NOINTERRUPT is 1, then the I2C routines are not interruptible. +// This is most probably only necessary if you are using a 1MHz system clock, +// you are communicating with a SMBus device, and you want to avoid timeouts. +// Be aware that the interrupt bit is enabled after each call. So the +// I2C functions should not be called in interrupt routines or critical regions. +#if !defined(I2C_NOINTERRUPT) +#define I2C_NOINTERRUPT 0 +#endif + +// I2C_TIMEOUT can be set to a value between 1 and 10000. +// If it is defined and nonzero, it leads to a timeout if the +// SCL is low longer than I2C_TIMEOUT milliseconds, i.e., max timeout is 10 sec +#if !defined(I2C_TIMEOUT) +#define I2C_TIMEOUT 0 +#else +#if I2C_TIMEOUT > 10000 +#error I2C_TIMEOUT is too large +#endif +#endif + +// I2C_MAXWAIT can be set to any value between 0 and 32767. 0 means no time out. +#if !defined(I2C_MAXWAIT) +#define I2C_MAXWAIT 500 +#else +#if I2C_MAXWAIT > 32767 || I2C_MAXWAIT < 0 +#error Illegal I2C_MAXWAIT value +#endif +#endif + +#define I2C_TIMEOUT_DELAY_LOOPS (I2C_CPUFREQ/1000UL)*I2C_TIMEOUT/4000UL +#if I2C_TIMEOUT_DELAY_LOOPS < 1 +#define I2C_MAX_STRETCH 1 +#else +#if I2C_TIMEOUT_DELAY_LOOPS > 60000UL +#define I2C_MAX_STRETCH 60000UL +#else +#define I2C_MAX_STRETCH I2C_TIMEOUT_DELAY_LOOPS +#endif +#endif + +#if I2C_FASTMODE +#define I2C_DELAY_COUNTER (((I2C_CPUFREQ/350000L)/2-18)/3) +#define SCL_CLOCK 400000UL +#else +#if I2C_SLOWMODE +#define I2C_DELAY_COUNTER (((I2C_CPUFREQ/23500L)/2-18)/3) +#define SCL_CLOCK 25000UL +#else +#define I2C_DELAY_COUNTER (((I2C_CPUFREQ/90000L)/2-18)/3) +#define SCL_CLOCK 100000UL +#endif +#endif + +#if !I2C_HARDWARE +// map the IO register back into the IO address space +#define SDA_DDR (_SFR_IO_ADDR(SDA_PORT) - 1) +#define SCL_DDR (_SFR_IO_ADDR(SCL_PORT) - 1) +#define SDA_OUT _SFR_IO_ADDR(SDA_PORT) +#define SCL_OUT _SFR_IO_ADDR(SCL_PORT) +#define SDA_IN (_SFR_IO_ADDR(SDA_PORT) - 2) +#define SCL_IN (_SFR_IO_ADDR(SCL_PORT) - 2) + +#ifndef __tmp_reg__ +#define __tmp_reg__ 0 +#endif + +// Internal delay functions. +void __attribute__ ((noinline)) i2c_delay_half(void) asm("ass_i2c_delay_half") __attribute__ ((used)); +void __attribute__ ((noinline)) i2c_wait_scl_high(void) asm("ass_i2c_wait_scl_high") __attribute__ ((used)); + +void i2c_delay_half(void) { // function call 3 cycles => 3C +#if I2C_DELAY_COUNTER < 1 + __asm__ __volatile__ (" ret"); + // 7 cycles for call and return +#else + __asm__ __volatile__ + ( + " ldi r25, %[DELAY] ;load delay constant ;; 4C \n\t" + "_Lidelay: \n\t" + " dec r25 ;decrement counter ;; 4C+xC \n\t" + " brne _Lidelay ;;5C+(x-1)2C+xC\n\t" + " ret ;; 9C+(x-1)2C+xC = 7C+xC" + : : [DELAY] "M" I2C_DELAY_COUNTER : "r25"); + // 7 cycles + 3 times x cycles +#endif +} + +void i2c_wait_scl_high(void) { +#if I2C_TIMEOUT <= 0 + __asm__ __volatile__ + ("_Li2c_wait_stretch: \n\t" + " sbis %[SCLIN],%[SCLPIN] ;wait for SCL high \n\t" +#if __AVR_HAVE_JMP_CALL__ + " jmp _Li2c_wait_stretch \n\t" +#else + " rjmp _Li2c_wait_stretch \n\t" +#endif + " cln ;signal: no timeout \n\t" + " ret " + : : [SCLIN] "I" (SCL_IN), [SCLPIN] "I" (SCL_PIN)); +#else // I2C_TIMEOUT <= 0 + __asm__ __volatile__ + ( " ldi r27, %[HISTRETCH] ;load delay counter \n\t" + " ldi r26, %[LOSTRETCH] \n\t" + "_Lwait_stretch: \n\t" + " clr __tmp_reg__ ;do next loop 255 times \n\t" + "_Lwait_stretch_inner_loop: \n\t" +#if __AVR_HAVE_JMP_CALL__ + " call _Lcheck_scl_level ;call check function ;; 12C \n\t" +#else + " rcall _Lcheck_scl_level ;call check function ;; 12C \n\t" +#endif + " brpl _Lstretch_done ;done if N=0 ;; +1 = 13C\n\t" + " dec __tmp_reg__ ;dec inner loop counter;; +1 = 14C\n\t" + " brne _Lwait_stretch_inner_loop ;; +2 = 16C\n\t" + " sbiw r26,1 ;dec outer loop counter \n\t" + " brne _Lwait_stretch ;continue with outer loop \n\t" + " sen ;timeout -> set N-bit=1 \n\t" +#if __AVR_HAVE_JMP_CALL__ + " jmp _Lwait_return ;and return with N=1\n\t" +#else + " rjmp _Lwait_return ;and return with N=1\n\t" +#endif + "_Lstretch_done: ;SCL=1 sensed \n\t" + " cln ;OK -> clear N-bit \n\t" +#if __AVR_HAVE_JMP_CALL__ + " jmp _Lwait_return ; and return with N=0 \n\t" +#else + " rjmp _Lwait_return ; and return with N=0 \n\t" +#endif + "_Lcheck_scl_level: ;; call = 3C\n\t" + " cln ;; +1C = 4C \n\t" + " sbic %[SCLIN],%[SCLPIN] ;skip if SCL still low ;; +2C = 6C \n\t" +#if __AVR_HAVE_JMP_CALL__ + " jmp _Lscl_high ;; +0C = 6C \n\t" +#else + " rjmp _Lscl_high ;; +0C = 6C \n\t" +#endif + " sen ;; +1 = 7C\n\t " + "_Lscl_high: " + " nop ;; +1C = 8C \n\t" + " ret ;return N-Bit=1 if low ;; +4 = 12C\n\t" + + "_Lwait_return:" + : : [SCLIN] "I" (SCL_IN), [SCLPIN] "I" (SCL_PIN), + [HISTRETCH] "M" (I2C_MAX_STRETCH>>8), + [LOSTRETCH] "M" (I2C_MAX_STRETCH&0xFF) + : "r26", "r27"); +#endif // I2C_TIMEOUT <= 0 +} +#endif // !I2C_HARDWARE + +bool i2c_init(void) +#if I2C_HARDWARE +{ +#if __has_include("digitalWriteFast.h") +#include "digitalWriteFast.h" +# if I2C_PULLUP + digitalWriteFast(SDA, 1); + digitalWriteFast(SCL, 1); +# else + digitalWriteFast(SDA, 0); + digitalWriteFast(SCL, 0); +# endif +#else // __has_include("digitalWriteFast.h") +# if I2C_PULLUP + digitalWrite(SDA, 1); + digitalWrite(SCL, 1); +# else + digitalWrite(SDA, 0); + digitalWrite(SCL, 0); +# endif +#endif // __has_include("digitalWriteFast.h") +#if ((I2C_CPUFREQ/SCL_CLOCK)-16)/2 < 250 + TWSR = 0; /* no prescaler */ + TWBR = ((I2C_CPUFREQ/SCL_CLOCK)-16)/2; /* must be > 10 for stable operation */ +#else + TWSR = (1< I2C_TIMEOUT) return false; +#endif + } + + // check value of TWI Status Register. Mask prescaler bits. + twst = TW_STATUS & 0xF8; + if ( (twst != TW_START) && (twst != TW_REP_START)) return false; + + // send device address + TWDR = addr; + TWCR = (1< I2C_TIMEOUT) return false; +#endif + } + + // check value of TWI Status Register. Mask prescaler bits. + twst = TW_STATUS & 0xF8; + if ( (twst != TW_MT_SLA_ACK) && (twst != TW_MR_SLA_ACK) ) return false; + + return true; +} +#else // I2C_HARDWARE + { + __asm__ __volatile__ + ( +#if I2C_NOINTERRUPT + " cli ;clear IRQ bit \n\t" +#endif + " sbis %[SCLIN],%[SCLPIN] ;check for clock stretching slave\n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_wait_scl_high ;wait until SCL=H\n\t" +#else + " rcall ass_i2c_wait_scl_high ;wait until SCL=H\n\t" +#endif +#if I2C_PULLUP + " cbi %[SDAOUT],%[SDAPIN] ;disable pull-up \n\t" +#endif + " sbi %[SDADDR],%[SDAPIN] ;force SDA low \n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;wait T/2 \n\t" + " call ass_i2c_write ;now write address \n\t" +#else + " rcall ass_i2c_delay_half ;wait T/2 \n\t" + " rcall ass_i2c_write ;now write address \n\t" +#endif + " ret" + : : [SDADDR] "I" (SDA_DDR), [SDAPIN] "I" (SDA_PIN), + [SDAOUT] "I" (SDA_OUT), [SCLOUT] "I" (SCL_OUT), + [SCLIN] "I" (SCL_IN),[SCLPIN] "I" (SCL_PIN)); + return true; // we never return here! +} +#endif // I2C_HARDWARE + +bool i2c_rep_start(uint8_t addr) +#if I2C_HARDWARE +{ + return i2c_start(addr); +} +#else // I2C_HARDWARE + { + __asm__ __volatile__ + + ( +#if I2C_NOINTERRUPT + " cli \n\t" +#endif +#if I2C_PULLUP + " cbi %[SCLOUT],%[SCLPIN] ;disable SCL pull-up \n\t" +#endif + " sbi %[SCLDDR],%[SCLPIN] ;force SCL low \n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 \n\t" +#else + " rcall ass_i2c_delay_half ;delay T/2 \n\t" +#endif + " cbi %[SDADDR],%[SDAPIN] ;release SDA \n\t" +#if I2C_PULLUP + " sbi %[SDAOUT],%[SDAPIN] ;enable SDA pull-up \n\t" +#endif +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 \n\t" +#else + " rcall ass_i2c_delay_half ;delay T/2 \n\t" +#endif + " cbi %[SCLDDR],%[SCLPIN] ;release SCL \n\t" +#if I2C_PULLUP + " sbi %[SCLOUT],%[SCLPIN] ;enable SCL pull-up \n\t" +#endif +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 \n\t" +#else + " rcall ass_i2c_delay_half ;delay T/2 \n\t" +#endif + " sbis %[SCLIN],%[SCLPIN] ;check for clock stretching slave\n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_wait_scl_high ;wait until SCL=H\n\t" +#else + " rcall ass_i2c_wait_scl_high ;wait until SCL=H\n\t" +#endif +#if I2C_PULLUP + " cbi %[SDAOUT],%[SDAPIN] ;disable SDA pull-up\n\t" +#endif + " sbi %[SDADDR],%[SDAPIN] ;force SDA low \n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 \n\t" + " call ass_i2c_write \n\t" +#else + " rcall ass_i2c_delay_half ;delay T/2 \n\t" + " rcall ass_i2c_write \n\t" +#endif + " ret" + : : [SCLDDR] "I" (SCL_DDR), [SCLPIN] "I" (SCL_PIN), + [SCLIN] "I" (SCL_IN), [SCLOUT] "I" (SCL_OUT), [SDAOUT] "I" (SDA_OUT), + [SDADDR] "I" (SDA_DDR), [SDAPIN] "I" (SDA_PIN)); + return true; // just to fool the compiler +} +#endif // I2C_HARDWARE + +bool i2c_start_wait(uint8_t addr) +#if I2C_HARDWARE +{ + uint8_t twst; + uint16_t maxwait = I2C_MAXWAIT; +#if I2C_TIMEOUT + uint32_t start = millis(); +#endif + + while (true) { + // send START condition + TWCR = (1< I2C_TIMEOUT) return false; +#endif + } + + // check value of TWI Status Register. Mask prescaler bits. + twst = TW_STATUS & 0xF8; + if ( (twst != TW_START) && (twst != TW_REP_START)) continue; + + // send device address + TWDR = addr; + TWCR = (1< I2C_TIMEOUT) return false; +#endif + } + + // check value of TWI Status Register. Mask prescaler bits. + twst = TW_STATUS & 0xF8; + if ( (twst == TW_MT_SLA_NACK )||(twst ==TW_MR_DATA_NACK) ) + { + /* device busy, send stop condition to terminate write operation */ + TWCR = (1< I2C_TIMEOUT) return false; +#endif + } + + if (maxwait && --maxwait == 0) return false; + + continue; + } + //if( twst != TW_MT_SLA_ACK) return 1; + return true; + } +} +#else // I2C_HARDWARE + { + __asm__ __volatile__ + ( + " push r24 ;save original parameter \n\t" +#if I2C_MAXWAIT + " ldi r31, %[HIMAXWAIT] ;load max wait counter \n\t" + " ldi r30, %[LOMAXWAIT] ;load low byte \n\t" +#endif + "_Li2c_start_wait1: \n\t" + " pop r24 ;restore original parameter\n\t" + " push r24 ;and save again \n\t" +#if I2C_NOINTERRUPT + " cli ;disable interrupts \n\t" +#endif + " sbis %[SCLIN],%[SCLPIN] ;check for clock stretching slave\n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_wait_scl_high ;wait until SCL=H\n\t" +#else + " rcall ass_i2c_wait_scl_high ;wait until SCL=H\n\t" +#endif +#if I2C_PULLUP + " cbi %[SDAOUT],%[SDAPIN] ;disable pull-up \n\t" +#endif + " sbi %[SDADDR],%[SDAPIN] ;force SDA low \n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 \n\t" + " call ass_i2c_write ;write address \n\t" +#else + " rcall ass_i2c_delay_half ;delay T/2 \n\t" + " rcall ass_i2c_write ;write address \n\t" +#endif + " tst r24 ;if device not busy -> done \n\t" + " brne _Li2c_start_wait_done \n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_stop ;terminate write & enable IRQ \n\t" +#else + " rcall ass_i2c_stop ;terminate write & enable IRQ \n\t" +#endif +#if I2C_MAXWAIT + " sbiw r30,1 ;decrement max wait counter\n\t" + " breq _Li2c_start_wait_done ;if zero reached, exit with false -> r24 already zero!\n\t" +#endif +#if __AVR_HAVE_JMP_CALL__ + " jmp _Li2c_start_wait1 ;device busy, poll ack again \n\t" +#else + " rjmp _Li2c_start_wait1 ;device busy, poll ack again \n\t" +#endif + "_Li2c_start_wait_done: \n\t" + " clr r25 ;clear high byte of return value\n\t" + " pop __tmp_reg__ ;pop off orig argument \n\t" + " ret " + : : [SDADDR] "I" (SDA_DDR), [SDAPIN] "I" (SDA_PIN), [SDAOUT] "I" (SDA_OUT), + [SCLIN] "I" (SCL_IN), [SCLPIN] "I" (SCL_PIN), + [HIMAXWAIT] "M" (I2C_MAXWAIT>>8), + [LOMAXWAIT] "M" (I2C_MAXWAIT&0xFF) + : "r30", "r31" ); + return true; // fooling the compiler +} +#endif // I2C_HARDWARE + +void i2c_stop(void) +#if I2C_HARDWARE +{ +#if I2C_TIMEOUT + uint32_t start = millis(); +#endif + /* send stop condition */ + TWCR = (1< I2C_TIMEOUT) return; +#endif + } +} +#else // I2C_HARDWARE + { + __asm__ __volatile__ + ( +#if I2C_PULLUP + " cbi %[SCLOUT],%[SCLPIN] ;disable SCL pull-up \n\t" +#endif + " sbi %[SCLDDR],%[SCLPIN] ;force SCL low \n\t" +#if I2C_PULLUP + " cbi %[SDAOUT],%[SDAPIN] ;disable pull-up \n\t" +#endif + " sbi %[SDADDR],%[SDAPIN] ;force SDA low \n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;T/2 delay \n\t" +#else + " rcall ass_i2c_delay_half ;T/2 delay \n\t" +#endif + " cbi %[SCLDDR],%[SCLPIN] ;release SCL \n\t" +#if I2C_PULLUP + " sbi %[SCLOUT],%[SCLPIN] ;enable SCL pull-up \n\t" +#endif +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;T/2 delay \n\t" +#else + " rcall ass_i2c_delay_half ;T/2 delay \n\t" +#endif + " sbis %[SCLIN],%[SCLPIN] ;check for clock stretching slave\n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_wait_scl_high ;wait until SCL=H\n\t" +#else + " rcall ass_i2c_wait_scl_high ;wait until SCL=H\n\t" +#endif + " cbi %[SDADDR],%[SDAPIN] ;release SDA \n\t" +#if I2C_PULLUP + " sbi %[SDAOUT],%[SDAPIN] ;enable SDA pull-up \n\t" +#endif +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half \n\t" +#else + " rcall ass_i2c_delay_half \n\t" +#endif +#if I2C_NOINTERRUPT + " sei ;enable interrupts again!\n\t" +#endif + : : [SCLDDR] "I" (SCL_DDR), [SCLPIN] "I" (SCL_PIN), [SCLIN] "I" (SCL_IN), + [SDAOUT] "I" (SDA_OUT), [SCLOUT] "I" (SCL_OUT), + [SDADDR] "I" (SDA_DDR), [SDAPIN] "I" (SDA_PIN)); +} +#endif // I2C_HARDWARE + +bool i2c_write(uint8_t value) +#if I2C_HARDWARE +{ + uint8_t twst; +#if I2C_TIMEOUT + uint32_t start = millis(); +#endif + + // send data to the previously addressed device + TWDR = value; + TWCR = (1< I2C_TIMEOUT) return false; +#endif + } + + // check value of TWI Status Register. Mask prescaler bits + twst = TW_STATUS & 0xF8; + if( twst != TW_MT_DATA_ACK) return false; + return true; +} +#else // I2C_HARDWARE + { + __asm__ __volatile__ + ( + " sec ;set carry flag \n\t" + " rol r24 ;shift in carry and shift out MSB \n\t" +#if __AVR_HAVE_JMP_CALL__ + " jmp _Li2c_write_first \n\t" +#else + " rjmp _Li2c_write_first \n\t" +#endif + "_Li2c_write_bit:\n\t" + " lsl r24 ;left shift into carry ;; 1C\n\t" + "_Li2c_write_first:\n\t" + " breq _Li2c_get_ack ;jump if TXreg is empty;; +1 = 2C \n\t" +#if I2C_PULLUP + " cbi %[SCLOUT],%[SCLPIN] ;disable SCL pull-up \n\t" +#endif + " sbi %[SCLDDR],%[SCLPIN] ;force SCL low ;; +2 = 4C \n\t" + " nop \n\t" + " nop \n\t" + " nop \n\t" + " brcc _Li2c_write_low ;;+1/+2=5/6C\n\t" + " nop ;; +1 = 7C \n\t" + " cbi %[SDADDR],%[SDAPIN] ;release SDA ;; +2 = 9C \n\t" +#if I2C_PULLUP + " sbi %[SDAOUT],%[SDAPIN] ;enable SDA pull-up \n\t" +#endif +#if __AVR_HAVE_JMP_CALL__ + " jmp _Li2c_write_high ;; +2 = 11C \n\t" +#else + " rjmp _Li2c_write_high ;; +2 = 11C \n\t" +#endif + "_Li2c_write_low: \n\t" +#if I2C_PULLUP + " cbi %[SDAOUT],%[SDAPIN] ;disable pull-up \n\t" +#endif + " sbi %[SDADDR],%[SDAPIN] ;force SDA low ;; +2 = 9C \n\t" +#if __AVR_HAVE_JMP_CALL__ + " jmp _Li2c_write_high ;;+2 = 11C \n\t" +#else + " rjmp _Li2c_write_high ;;+2 = 11C \n\t" +#endif + "_Li2c_write_high: \n\t" +#if I2C_DELAY_COUNTER >= 1 +# if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 ;;+X = 11C+X\n\t" +# else + " rcall ass_i2c_delay_half ;delay T/2 ;;+X = 11C+X\n\t" +# endif +#endif + " cbi %[SCLDDR],%[SCLPIN] ;release SCL ;;+2 = 13C+X\n\t" +#if I2C_PULLUP + " sbi %[SCLOUT],%[SCLPIN] ;enable SCL pull-up \n\t" +#endif + " cln ;clear N-bit ;;+1 = 14C+X\n\t" + " nop \n\t" + " nop \n\t" + " nop \n\t" + " sbis %[SCLIN],%[SCLPIN] ;check for SCL high ;;+2 = 16C+X\n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_wait_scl_high \n\t" +#else + " rcall ass_i2c_wait_scl_high \n\t" +#endif + " brpl _Ldelay_scl_high ;;+2 = 18C+X\n\t" + "_Li2c_write_return_false: \n\t" + " clr r24 ; return false because of timeout \n\t" +#if __AVR_HAVE_JMP_CALL__ + " jmp _Li2c_write_return \n\t" +#else + " rjmp _Li2c_write_return \n\t" +#endif + "_Ldelay_scl_high: \n\t" +#if I2C_DELAY_COUNTER >= 1 +# if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 ;;+X= 18C+2X\n\t" +# else + " rcall ass_i2c_delay_half ;delay T/2 ;;+X= 18C+2X\n\t" +# endif +#endif +#if __AVR_HAVE_JMP_CALL__ + " jmp _Li2c_write_bit \n\t" +#else + " rjmp _Li2c_write_bit \n\t" +#endif + " ;; +2 = 20C +2X for one bit-loop \n\t" + "_Li2c_get_ack: \n\t" +#if I2C_PULLUP + " cbi %[SCLOUT],%[SCLPIN] ;disable SCL pull-up \n\t" +#endif + " sbi %[SCLDDR],%[SCLPIN] ;force SCL low ;; +2 = 5C \n\t" + " nop \n\t" + " nop \n\t" + " cbi %[SDADDR],%[SDAPIN] ;release SDA ;;+2 = 7C \n\t" +#if I2C_PULLUP + " sbi %[SDAOUT],%[SDAPIN] ;enable SDA pull-up \n\t" +#endif +#if I2C_DELAY_COUNTER >= 1 +# if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 ;; +X = 7C+X \n\t" +# else + " rcall ass_i2c_delay_half ;delay T/2 ;; +X = 7C+X \n\t" +# endif +#endif + " clr r25 ;; 17C+2X \n\t" + " clr r24 ;return 0 ;; 14C + X \n\t" + " cbi %[SCLDDR],%[SCLPIN] ;release SCL ;; +2 = 9C+X\n\t" +#if I2C_PULLUP + " sbi %[SCLOUT],%[SCLPIN] ;enable SCL pull-up \n\t" +#endif + "_Li2c_ack_wait: \n\t" + " cln ; clear N-bit ;; 10C + X\n\t" + " nop \n\t" + " sbis %[SCLIN],%[SCLPIN] ;wait SCL high ;; 12C + X \n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_wait_scl_high \n\t" +#else + " rcall ass_i2c_wait_scl_high \n\t" +#endif + " brmi _Li2c_write_return_false ;; 13C + X \n\t " + " sbis %[SDAIN],%[SDAPIN] ;if SDA hi -> return 0 ;; 15C + X \n\t" + " ldi r24,1 ;return true ;; 16C + X \n\t" +#if I2C_DELAY_COUNTER >= 1 +# if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 ;; 16C + 2X \n\t" +# else + " rcall ass_i2c_delay_half ;delay T/2 ;; 16C + 2X \n\t" +# endif +#endif + "_Li2c_write_return: \n\t" + " nop \n\t " + " nop \n\t " +#if I2C_PULLUP + " cbi %[SCLOUT],%[SCLPIN] ;disable SCL pull-up \n\t" +#endif + " sbi %[SCLDDR],%[SCLPIN] ;force SCL low so SCL=H is short\n\t" + " ret \n\t" + " ;; + 4 = 17C + 2X for acknowldge bit" + :: + [SCLDDR] "I" (SCL_DDR), [SCLPIN] "I" (SCL_PIN), [SCLIN] "I" (SCL_IN), + [SDAOUT] "I" (SDA_OUT), [SCLOUT] "I" (SCL_OUT), + [SDADDR] "I" (SDA_DDR), [SDAPIN] "I" (SDA_PIN), [SDAIN] "I" (SDA_IN)); + return true; // fooling the compiler +} +#endif // I2C_HARDWARE + +uint8_t i2c_read(bool last) +#if I2C_HARDWARE +{ +#if I2C_TIMEOUT + uint32_t start = millis(); +#endif + + TWCR = (1< I2C_TIMEOUT) return 0xFF; +#endif + } + return TWDR; +} +#else // I2C_HARDWARE + { + __asm__ __volatile__ + ( + " ldi r23,0x01 \n\t" + "_Li2c_read_bit: \n\t" +#if I2C_PULLUP + " cbi %[SCLOUT],%[SCLPIN] ;disable SCL pull-up \n\t" +#endif + " sbi %[SCLDDR],%[SCLPIN] ;force SCL low ;; 2C \n\t" + " cbi %[SDADDR],%[SDAPIN] ;release SDA(prev. ACK);; 4C \n\t" +#if I2C_PULLUP + " sbi %[SDAOUT],%[SDAPIN] ;enable SDA pull-up \n\t" +#endif + " nop \n\t" + " nop \n\t" + " nop \n\t" +#if I2C_DELAY_COUNTER >= 1 +# if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 ;; 4C+X \n\t" +# else + " rcall ass_i2c_delay_half ;delay T/2 ;; 4C+X \n\t" +# endif +#endif + " cbi %[SCLDDR],%[SCLPIN] ;release SCL ;; 6C + X \n\t" +#if I2C_PULLUP + " sbi %[SCLOUT],%[SCLPIN] ;enable SCL pull-up \n\t" +#endif +#if I2C_DELAY_COUNTER >= 1 +# if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 ;; 6C + 2X \n\t" +# else + " rcall ass_i2c_delay_half ;delay T/2 ;; 6C + 2X \n\t" +# endif +#endif + " cln ; clear N-bit ;; 7C + 2X \n\t" + " nop \n\t " + " nop \n\t " + " nop \n\t " + " sbis %[SCLIN], %[SCLPIN] ;check for SCL high ;; 9C +2X \n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_wait_scl_high \n\t" +#else + " rcall ass_i2c_wait_scl_high \n\t" +#endif + " brmi _Li2c_read_return ;return if timeout ;; 10C + 2X\n\t" + " clc ;clear carry flag ;; 11C + 2X\n\t" + " sbic %[SDAIN],%[SDAPIN] ;if SDA is high ;; 11C + 2X\n\t" + " sec ;set carry flag ;; 12C + 2X\n\t" + " rol r23 ;store bit ;; 13C + 2X\n\t" + " brcc _Li2c_read_bit ;while receiv reg not full \n\t" + " ;; 15C + 2X for one bit loop \n\t" + + "_Li2c_put_ack: \n\t" +#if I2C_PULLUP + " cbi %[SCLOUT],%[SCLPIN] ;disable SCL pull-up \n\t" +#endif + " sbi %[SCLDDR],%[SCLPIN] ;force SCL low ;; 2C \n\t" + " cpi r24,0 ;; 3C \n\t" + " breq _Li2c_put_ack_low ;if (ack=0) ;; 5C \n\t" + " cbi %[SDADDR],%[SDAPIN] ;release SDA \n\t" +#if I2C_PULLUP + " sbi %[SDAOUT],%[SDAPIN] ;enable SDA pull-up \n\t" +#endif +#if __AVR_HAVE_JMP_CALL__ + " jmp _Li2c_put_ack_high \n\t" +#else + " rjmp _Li2c_put_ack_high \n\t" +#endif + "_Li2c_put_ack_low: ;else \n\t" +#if I2C_PULLUP + " cbi %[SDAOUT],%[SDAPIN] ;disable pull-up \n\t" +#endif + " sbi %[SDADDR],%[SDAPIN] ;force SDA low ;; 7C \n\t" + "_Li2c_put_ack_high: \n\t" + " nop \n\t " + " nop \n\t " + " nop \n\t " +#if I2C_DELAY_COUNTER >= 1 +# if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 ;; 7C + X \n\t" +# else + " rcall ass_i2c_delay_half ;delay T/2 ;; 7C + X \n\t" +# endif +#endif + " cbi %[SCLDDR],%[SCLPIN] ;release SCL ;; 9C +X \n\t" +#if I2C_PULLUP + " sbi %[SCLOUT],%[SCLPIN] ;enable SCL pull-up \n\t" +#endif + " cln ;clear N ;; +1 = 10C\n\t" + " nop \n\t " + " nop \n\t " + " sbis %[SCLIN],%[SCLPIN] ;wait SCL high ;; 12C + X\n\t" +#if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_wait_scl_high \n\t" +#else + " rcall ass_i2c_wait_scl_high \n\t" +#endif +#if I2C_DELAY_COUNTER >= 1 +# if __AVR_HAVE_JMP_CALL__ + " call ass_i2c_delay_half ;delay T/2 ;; 11C + 2X\n\t" +# else + " rcall ass_i2c_delay_half ;delay T/2 ;; 11C + 2X\n\t" +#endif +#endif + "_Li2c_read_return: \n\t" + " nop \n\t " + " nop \n\t " +#if I2C_PULLUP + " cbi %[SCLOUT],%[SCLPIN] ;disable SCL pull-up \n\t" +#endif + "sbi %[SCLDDR],%[SCLPIN] ;force SCL low so SCL=H is short\n\t" + " mov r24,r23 ;; 12C + 2X \n\t" + " clr r25 ;; 13 C + 2X\n\t" + " ret ;; 17C + X" + :: + [SCLDDR] "I" (SCL_DDR), [SCLPIN] "I" (SCL_PIN), [SCLIN] "I" (SCL_IN), + [SDAOUT] "I" (SDA_OUT), [SCLOUT] "I" (SCL_OUT), + [SDADDR] "I" (SDA_DDR), [SDAPIN] "I" (SDA_PIN), [SDAIN] "I" (SDA_IN) + ); + return ' '; // fool the compiler! +} +#endif // I2C_HARDWARE + +#pragma GCC diagnostic pop // GCC diagnostic ignored "-Wunused-parameter" + +#if !defined(_WORD_UNION_H) +/** + * Union to specify parts / manifestations of a 16 bit Word without casts and shifts. + * It also supports the compiler generating small code. + */ +union WordUnion { + struct { + uint8_t LowByte; + uint8_t HighByte; + } UByte; + struct { + int8_t LowByte; + int8_t HighByte; + } Byte; + uint8_t UBytes[2]; + int8_t Bytes[2]; + uint16_t UWord; + int16_t Word; + uint8_t *BytePointer; +}; +#define _WORD_UNION_H +#endif + +void i2c_write_byte(uint8_t addr, uint8_t byte) { + i2c_start(addr); + i2c_write(byte); + i2c_stop(); +} + +void i2c_read_buffer_from_register(uint8_t addr, uint8_t register_number, uint8_t * byte_buffer, uint8_t number_of_bytes_to_read){ + i2c_start (addr); + i2c_write(register_number); + i2c_rep_start(addr | I2C_READ); // restart for reading + int8_t i = 0; + for (; i < number_of_bytes_to_read - 1; ++i) { + byte_buffer[i] = i2c_read(false); + } + byte_buffer[i] = i2c_read(true); +} + +void i2c_read_buffer_from_16bit_register(uint8_t addr, uint16_t register_number, uint8_t * byte_buffer, uint8_t number_of_bytes_to_read){ + i2c_start (addr); + i2c_write(register_number >> 8); + i2c_write(register_number & 0xFF); + i2c_rep_start(addr | I2C_READ); // restart for reading + int8_t i = 0; + for (; i < number_of_bytes_to_read - 1; ++i) { + byte_buffer[i] = i2c_read(false); + } + byte_buffer[i] = i2c_read(true); +} + +void i2c_write_buffer_to_16bit_register(uint8_t addr, uint16_t register_number, uint8_t * byte_buffer, uint8_t number_of_bytes_to_write) { + i2c_start (addr); + i2c_write(register_number >> 8); + i2c_write(register_number & 0xFF); + for (uint8_t i = 0; i < number_of_bytes_to_write; i++) { + i2c_write (byte_buffer[i]); + } + i2c_stop(); +} + +void i2c_write_buffer_to_register(uint8_t addr, uint8_t register_number, uint8_t * byte_buffer, uint8_t number_of_bytes_to_write) { + i2c_start (addr); + i2c_write(register_number); + for (uint8_t i = 0; i < number_of_bytes_to_write; i++) { + i2c_write (byte_buffer[i]); + } + i2c_stop(); +} + +void i2c_write_byte_to_register(uint8_t addr, uint8_t register_number, uint8_t byte) { + i2c_start(addr); + i2c_write(register_number); + i2c_write(byte); + i2c_stop(); +} + +uint16_t i2c_read_word_from_register(uint8_t addr, uint8_t register_number) { + WordUnion tWord; + + i2c_start(addr); + i2c_write(register_number); + i2c_rep_start(addr | I2C_READ); // restart for reading + tWord.UByte.LowByte = i2c_read(false); + tWord.UByte.HighByte = i2c_read(true); + return tWord.UWord; +} + +uint16_t i2c_read_word_swapped_from_register(uint8_t addr, uint8_t register_number) { + WordUnion tWord; // Using WordUnion saves 16 bytes program space + + i2c_start(addr); + i2c_write(register_number); + i2c_rep_start(addr | I2C_READ); // restart for reading + tWord.UByte.HighByte = i2c_read(false); + tWord.UByte.LowByte = i2c_read(true); + return tWord.UWord; +} + +#endif // #ifndef _SOFTI2C_H +#endif // !defined(USE_SOFT_I2C_MASTER_H_AS_PLAIN_INCLUDE) +#endif // #ifndef __AVR_ARCH__ diff --git a/JK-BMSToPylontechCAN/SoftI2CMasterConfig.h b/JK-BMSToPylontechCAN/SoftI2CMasterConfig.h new file mode 100644 index 0000000..9a84e6a --- /dev/null +++ b/JK-BMSToPylontechCAN/SoftI2CMasterConfig.h @@ -0,0 +1,71 @@ +/* Arduino SoftI2C library. + * + * SoftI2CMasterConfig.h + * + * This contains a sample configuration setting for SoftI2CMaster.h + * The existence of this file can trigger the use of SoftI2CMaster by usage of "#if __has_include("SoftI2CMasterConfig.h")", + * which saves 2110 bytes program memory and 200 bytes RAM compared with Arduino Wire. + * + * Copyright (C) 2022, Armin Joachimsmeyer + * + * This file is part of SoftI2CMaster https://github.com/felias-fogg/SoftI2CMaster. + * + * This Library 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 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. + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Arduino I2cMaster Library. If not, see + * . + */ + +/* In order to use the library, you need to define SDA_PIN, SCL_PIN, + * SDA_PORT and SCL_PORT before including this file. Have a look at + * http://www.arduino.cc/en/Reference/PortManipulation for finding out + * which values to use. For example, if you use digital pin 3 (corresponding + * to PD3) for SDA and digital pin 13 (corresponding to PB5) + * for SCL on a standard Arduino, + * you have to use the following definitions: + * #define SDA_PIN 3 + * #define SDA_PORT PORTD + * #define SCL_PIN 5 + * #define SCL_PORT PORTB + * + * Alternatively, you can define the compile time constant I2C_HARDWARE, + * in which case the TWI hardware is used. In this case you have to use + * the standard SDA/SCL pins (and, of course, the chip needs to support + * this). + * + * You can also define the following constants (see also below): + ' - I2C_PULLUP = 1 meaning that internal pullups should be used + * - I2C_CPUFREQ, when changing CPU clock frequency dynamically + * - I2C_FASTMODE = 1 meaning that the I2C bus allows speeds up to 400 kHz + * - I2C_SLOWMODE = 1 meaning that the I2C bus will allow only up to 25 kHz + * - I2C_NOINTERRUPT = 1 in order to prohibit interrupts while + * communicating (see below). This can be useful if you use the library + * for communicating with SMbus devices, which have timeouts. + * Note, however, that interrupts are disabled from issuing a start condition + * until issuing a stop condition. So use this option with care! + * - I2C_TIMEOUT = 0...10000 msec in order to return from the I2C functions + * in case of a I2C bus lockup (i.e., SCL constantly low). 0 means no timeout. + * - I2C_MAXWAIT = 0...32767 number of retries in i2c_start_wait. 0 means never stop. + */ +#ifndef _SOFT_I2C_MASTER_CONFIG_H +#define _SOFT_I2C_MASTER_CONFIG_H + +//#define SCL_PIN 5 +//#define SCL_PORT PORTC +//#define SDA_PIN 4 +//#define SDA_PORT PORTC +#define I2C_HARDWARE 1 // use I2C Hardware +#define I2C_PULLUP 1 +//#define I2C_TIMEOUT 5000 // costs 350 bytes +#define I2C_FASTMODE 1 + +#endif // _SOFT_I2C_MASTER_CONFIG_H diff --git a/JK-BMSToPylontechCAN/SoftwareSerialTX.cpp b/JK-BMSToPylontechCAN/SoftwareSerialTX.cpp new file mode 100644 index 0000000..3e9b7fc --- /dev/null +++ b/JK-BMSToPylontechCAN/SoftwareSerialTX.cpp @@ -0,0 +1,126 @@ +/* +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 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 + +The latest version of this library can always be found at +http://arduiniana.org. + +*/ + +#if defined(__AVR_ATmega168__) ||defined(__AVR_ATmega168P__) ||defined(__AVR_ATmega328P__) +// +// Includes +// +#include +#include +#include "SoftwareSerialTX.h" + +// +// Constructor +// +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 + + _delay_loop_2(delay); + b >>= 1; + } + + // restore pin to natural state + *reg |= reg_mask; + + 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); +} + +uint16_t SoftwareSerialTX::subtract_cap(uint16_t num, uint16_t sub) { + if (num > sub) + return num - sub; + else + return 1; +} + +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. + digitalWrite(tx, HIGH); + pinMode(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; +} +#else +#error unsupported platform +#endif \ No newline at end of file diff --git a/JK-BMSToPylontechCAN/SoftwareSerialTX.h b/JK-BMSToPylontechCAN/SoftwareSerialTX.h new file mode 100644 index 0000000..826e9fe --- /dev/null +++ b/JK-BMSToPylontechCAN/SoftwareSerialTX.h @@ -0,0 +1,56 @@ +/* +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 + ~ 686 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 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 + +The latest version of this library can always be found at +http://arduiniana.org. +*/ + +#ifndef SoftwareSerialTX_h +#define SoftwareSerialTX_h + +#include + +class SoftwareSerialTX +{ +private: + // per object data + uint8_t _transmitBitMask; + volatile uint8_t *_transmitPortRegister; + uint16_t _tx_delay; + + // private methods + void setTX(uint8_t transmitPin); + +#ifndef ARDUINO_RASPBERRY_PI_PICO + // Return num - sub, or 1 if the result would be < 1 + static uint16_t subtract_cap(uint16_t num, uint16_t sub); +#endif + +public: + // public methods + SoftwareSerialTX(uint8_t transmitPin); + void begin(long speed); + size_t write(uint8_t byte); + size_t write(const uint8_t *buffer, size_t size); +}; + +#endif diff --git a/JK-BMSToPylontechCAN/mcp2515_can_dfs.h b/JK-BMSToPylontechCAN/mcp2515_can_dfs.h new file mode 100644 index 0000000..eff68f0 --- /dev/null +++ b/JK-BMSToPylontechCAN/mcp2515_can_dfs.h @@ -0,0 +1,538 @@ +/* + mcp_can_dfs.h + 2012 Copyright (c) Seeed Technology Inc. All right reserved. + + Author:Loovee (loovee@seeed.cc) + 2014-1-16 + + Contributor: + + Cory J. Fowler + Latonita + Woodward1 + Mehtajaghvi + BykeBlast + TheRo0T + Tsipizic + ralfEdmund + Nathancheek + BlueAndi + Adlerweb + Btetz + Hurvajs + xboxpro1 + ttlappalainen + + The MIT License (MIT) + + Copyright (c) 2013 Seeed Technology Inc. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +#ifndef _MCP2515DFS_H_ +#define _MCP2515DFS_H_ + +#include +#include +#include + + +// if print debug information +#ifndef DEBUG_EN + #define DEBUG_EN 1 +#endif + +// Begin mt + +#define TIMEOUTVALUE 50 +#define MCP_SIDH 0 +#define MCP_SIDL 1 +#define MCP_EID8 2 +#define MCP_EID0 3 + +#define MCP_TXB_EXIDE_M 0x08 // In TXBnSIDL +#define MCP_DLC_MASK 0x0F // 4 LSBits +#define MCP_RTR_MASK 0x40 // (1<<6) Bit 6 + +#define MCP_RXB_RX_ANY 0x60 +#define MCP_RXB_RX_EXT 0x40 +#define MCP_RXB_RX_STD 0x20 +#define MCP_RXB_RX_STDEXT 0x00 +#define MCP_RXB_RX_MASK 0x60 +#define MCP_RXB_BUKT_MASK (1<<2) + + +// Bits in the TXBnCTRL registers. + +#define MCP_TXB_TXBUFE_M 0x80 +#define MCP_TXB_ABTF_M 0x40 +#define MCP_TXB_MLOA_M 0x20 +#define MCP_TXB_TXERR_M 0x10 +#define MCP_TXB_TXREQ_M 0x08 +#define MCP_TXB_TXIE_M 0x04 +#define MCP_TXB_TXP10_M 0x03 + +#define MCP_TXB_RTR_M 0x40 // In TXBnDLC +#define MCP_RXB_IDE_M 0x08 // In RXBnSIDL +#define MCP_RXB_RTR_M 0x40 // In RXBnDLC + +#define MCP_STAT_TX_PENDING_MASK (0x54) +#define MCP_STAT_TX0_PENDING (0x04) +#define MCP_STAT_TX1_PENDING (0x10) +#define MCP_STAT_TX2_PENDING (0x40) +#define MCP_STAT_TXIF_MASK (0xA8) +#define MCP_STAT_TX0IF (0x08) +#define MCP_STAT_TX1IF (0x20) +#define MCP_STAT_TX2IF (0x80) +#define MCP_STAT_RXIF_MASK (0x03) +#define MCP_STAT_RX0IF (1<<0) +#define MCP_STAT_RX1IF (1<<1) + +#define MCP_EFLG_RX1OVR (1<<7) +#define MCP_EFLG_RX0OVR (1<<6) +#define MCP_EFLG_TXBO (1<<5) +#define MCP_EFLG_TXEP (1<<4) +#define MCP_EFLG_RXEP (1<<3) +#define MCP_EFLG_TXWAR (1<<2) +#define MCP_EFLG_RXWAR (1<<1) +#define MCP_EFLG_EWARN (1<<0) +#define MCP_EFLG_ERRORMASK (0xF8) // 5 MS-Bits + +// Define MCP2515 register addresses + +#define MCP_RXF0SIDH 0x00 +#define MCP_RXF0SIDL 0x01 +#define MCP_RXF0EID8 0x02 +#define MCP_RXF0EID0 0x03 +#define MCP_RXF1SIDH 0x04 +#define MCP_RXF1SIDL 0x05 +#define MCP_RXF1EID8 0x06 +#define MCP_RXF1EID0 0x07 +#define MCP_RXF2SIDH 0x08 +#define MCP_RXF2SIDL 0x09 +#define MCP_RXF2EID8 0x0A +#define MCP_RXF2EID0 0x0B +#define MCP_BFPCTRL 0x0C +#define MCP_TXRTSCTRL 0x0D +#define MCP_CANSTAT 0x0E +#define MCP_CANCTRL 0x0F +#define MCP_RXF3SIDH 0x10 +#define MCP_RXF3SIDL 0x11 +#define MCP_RXF3EID8 0x12 +#define MCP_RXF3EID0 0x13 +#define MCP_RXF4SIDH 0x14 +#define MCP_RXF4SIDL 0x15 +#define MCP_RXF4EID8 0x16 +#define MCP_RXF4EID0 0x17 +#define MCP_RXF5SIDH 0x18 +#define MCP_RXF5SIDL 0x19 +#define MCP_RXF5EID8 0x1A +#define MCP_RXF5EID0 0x1B +#define MCP_TEC 0x1C +#define MCP_REC 0x1D +#define MCP_RXM0SIDH 0x20 +#define MCP_RXM0SIDL 0x21 +#define MCP_RXM0EID8 0x22 +#define MCP_RXM0EID0 0x23 +#define MCP_RXM1SIDH 0x24 +#define MCP_RXM1SIDL 0x25 +#define MCP_RXM1EID8 0x26 +#define MCP_RXM1EID0 0x27 +#define MCP_CNF3 0x28 +#define MCP_CNF2 0x29 +#define MCP_CNF1 0x2A +#define MCP_CANINTE 0x2B +#define MCP_CANINTF 0x2C +#define MCP_EFLG 0x2D +#define MCP_TXB0CTRL 0x30 +#define MCP_TXB0SIDH 0x31 +#define MCP_TXB0SIDL 0x32 +#define MCP_TXB0DLC 0x35 +#define MCP_TXB0D0 0x36 +#define MCP_TXB1CTRL 0x40 +#define MCP_TXB1SIDH 0x41 +#define MCP_TXB2CTRL 0x50 +#define MCP_TXB2SIDH 0x51 +#define MCP_RXB0CTRL 0x60 +#define MCP_RXB0SIDH 0x61 +#define MCP_RXB1CTRL 0x70 +#define MCP_RXB1SIDH 0x71 + +#define MCP_TX_INT 0x1C // Enable all transmit interrup ts +#define MCP_TX01_INT 0x0C // Enable TXB0 and TXB1 interru pts +#define MCP_RX_INT 0x03 // Enable receive interrupts +#define MCP_NO_INT 0x00 // Disable all interrupts + +#define MCP_TX01_MASK 0x14 +#define MCP_TX_MASK 0x54 + + +// Define SPI Instruction Set + +#define MCP_WRITE 0x02 +#define MCP_READ 0x03 +#define MCP_BITMOD 0x05 +#define MCP_LOAD_TX0 0x40 +#define MCP_LOAD_TX1 0x42 +#define MCP_LOAD_TX2 0x44 + +#define MCP_RTS_TX0 0x81 +#define MCP_RTS_TX1 0x82 +#define MCP_RTS_TX2 0x84 +#define MCP_RTS_ALL 0x87 +#define MCP_READ_RX0 0x90 +#define MCP_READ_RX1 0x94 +#define MCP_READ_STATUS 0xA0 +#define MCP_RX_STATUS 0xB0 +#define MCP_RESET 0xC0 + + +// CANCTRL Register Values + +#define MODE_NORMAL 0x00 +#define MODE_SLEEP 0x20 +#define MODE_LOOPBACK 0x40 +#define MODE_LISTENONLY 0x60 +#define MODE_CONFIG 0x80 +#define MODE_POWERUP 0xE0 +#define MODE_MASK 0xE0 +#define ABORT_TX 0x10 +#define MODE_ONESHOT 0x08 +#define CLKOUT_ENABLE 0x04 +#define CLKOUT_DISABLE 0x00 +#define CLKOUT_PS1 0x00 +#define CLKOUT_PS2 0x01 +#define CLKOUT_PS4 0x02 +#define CLKOUT_PS8 0x03 + + +// CNF1 Register Values + +#define SJW1 0x00 +#define SJW2 0x40 +#define SJW3 0x80 +#define SJW4 0xC0 + + +// CNF2 Register Values + +#define BTLMODE 0x80 +#define SAMPLE_1X 0x00 +#define SAMPLE_3X 0x40 + + +// CNF3 Register Values + +#define SOF_ENABLE 0x80 +#define SOF_DISABLE 0x00 +#define WAKFIL_ENABLE 0x40 +#define WAKFIL_DISABLE 0x00 + + +// CANINTF Register Bits + +#define MCP_RX0IF 0x01 +#define MCP_RX1IF 0x02 +#define MCP_TX0IF 0x04 +#define MCP_TX1IF 0x08 +#define MCP_TX2IF 0x10 +#define MCP_ERRIF 0x20 +#define MCP_WAKIF 0x40 +#define MCP_MERRF 0x80 + +// BFPCTRL Register Bits + +#define B1BFS 0x20 +#define B0BFS 0x10 +#define B1BFE 0x08 +#define B0BFE 0x04 +#define B1BFM 0x02 +#define B0BFM 0x01 + +// TXRTCTRL Register Bits + +#define B2RTS 0x20 +#define B1RTS 0x10 +#define B0RTS 0x08 +#define B2RTSM 0x04 +#define B1RTSM 0x02 +#define B0RTSM 0x01 + +// speed 20 MHz - values from https://www.kvaser.com/support/calculators/bit-timing-calculator/ + +#define MCP_20MHz_500kBPS_CFG1 (0x00) +#define MCP_20MHz_500kBPS_CFG2 (0xAC) +#define MCP_20MHz_500kBPS_CFG3 (0x07) + +// speed 16M + +#define MCP_16MHz_1000kBPS_CFG1 (0x00) +#define MCP_16MHz_1000kBPS_CFG2 (0xD0) +#define MCP_16MHz_1000kBPS_CFG3 (0x82) + +#define MCP_16MHz_800kBPS_CFG1 (0x40) +#define MCP_16MHz_800kBPS_CFG2 (0x92) +#define MCP_16MHz_800kBPS_CFG3 (0x02) + +#define MCP_16MHz_666kBPS_CFG1 (0x00) +#define MCP_16MHz_666kBPS_CFG2 (0xA0) +#define MCP_16MHz_666kBPS_CFG3 (0x04) + +#define MCP_16MHz_500kBPS_CFG1 (0x00) +#define MCP_16MHz_500kBPS_CFG2 (0xF0) +#define MCP_16MHz_500kBPS_CFG3 (0x86) + +#define MCP_16MHz_250kBPS_CFG1 (0x41) +#define MCP_16MHz_250kBPS_CFG2 (0xF1) +#define MCP_16MHz_250kBPS_CFG3 (0x85) + +#define MCP_16MHz_200kBPS_CFG1 (0x01) +#define MCP_16MHz_200kBPS_CFG2 (0xFA) +#define MCP_16MHz_200kBPS_CFG3 (0x87) + +#define MCP_16MHz_125kBPS_CFG1 (0x03) +#define MCP_16MHz_125kBPS_CFG2 (0xF0) +#define MCP_16MHz_125kBPS_CFG3 (0x86) + +#define MCP_16MHz_100kBPS_CFG1 (0x03) +#define MCP_16MHz_100kBPS_CFG2 (0xFA) +#define MCP_16MHz_100kBPS_CFG3 (0x87) + +#define MCP_16MHz_95kBPS_CFG1 (0x03) +#define MCP_16MHz_95kBPS_CFG2 (0xAD) +#define MCP_16MHz_95kBPS_CFG3 (0x07) + +#define MCP_16MHz_83k3BPS_CFG1 (0x03) +#define MCP_16MHz_83k3BPS_CFG2 (0xBE) +#define MCP_16MHz_83k3BPS_CFG3 (0x07) + +#define MCP_16MHz_80kBPS_CFG1 (0x03) +#define MCP_16MHz_80kBPS_CFG2 (0xFF) +#define MCP_16MHz_80kBPS_CFG3 (0x87) + +#define MCP_16MHz_50kBPS_CFG1 (0x07) +#define MCP_16MHz_50kBPS_CFG2 (0xFA) +#define MCP_16MHz_50kBPS_CFG3 (0x87) + +#define MCP_16MHz_40kBPS_CFG1 (0x07) +#define MCP_16MHz_40kBPS_CFG2 (0xFF) +#define MCP_16MHz_40kBPS_CFG3 (0x87) + +#define MCP_16MHz_33kBPS_CFG1 (0x09) +#define MCP_16MHz_33kBPS_CFG2 (0xBE) +#define MCP_16MHz_33kBPS_CFG3 (0x07) + +#define MCP_16MHz_31k25BPS_CFG1 (0x0F) +#define MCP_16MHz_31k25BPS_CFG2 (0xF1) +#define MCP_16MHz_31k25BPS_CFG3 (0x85) + +#define MCP_16MHz_25kBPS_CFG1 (0X0F) +#define MCP_16MHz_25kBPS_CFG2 (0XBA) +#define MCP_16MHz_25kBPS_CFG3 (0X07) + +#define MCP_16MHz_20kBPS_CFG1 (0x0F) +#define MCP_16MHz_20kBPS_CFG2 (0xFF) +#define MCP_16MHz_20kBPS_CFG3 (0x87) + +#define MCP_16MHz_10kBPS_CFG1 (0x1F) +#define MCP_16MHz_10kBPS_CFG2 (0xFF) +#define MCP_16MHz_10kBPS_CFG3 (0x87) + +#define MCP_16MHz_5kBPS_CFG1 (0x3F) +#define MCP_16MHz_5kBPS_CFG2 (0xFF) +#define MCP_16MHz_5kBPS_CFG3 (0x87) + + +// speed 12M + +#define MCP_12MHz_1000kBPS_CFG1 (0x00)// +#define MCP_12MHz_1000kBPS_CFG2 (0x88)// +#define MCP_12MHz_1000kBPS_CFG3 (0x01)// + +#define MCP_12MHz_666kBPS_CFG1 (0x00)// +#define MCP_12MHz_666kBPS_CFG2 (0x92)// +#define MCP_12MHz_666kBPS_CFG3 (0x01)// + +#define MCP_12MHz_500kBPS_CFG1 (0x00) +#define MCP_12MHz_500kBPS_CFG2 (0x9B) +#define MCP_12MHz_500kBPS_CFG3 (0x02) + +#define MCP_12MHz_250kBPS_CFG1 (0x00)// +#define MCP_12MHz_250kBPS_CFG2 (0xBF)// +#define MCP_12MHz_250kBPS_CFG3 (0x06)/// + +#define MCP_12MHz_200kBPS_CFG1 (0x01)// +#define MCP_12MHz_200kBPS_CFG2 (0xA4)// +#define MCP_12MHz_200kBPS_CFG3 (0x03)// + +#define MCP_12MHz_125kBPS_CFG1 (0x01)// +#define MCP_12MHz_125kBPS_CFG2 (0xBF)// +#define MCP_12MHz_125kBPS_CFG3 (0x06)// + +#define MCP_12MHz_100kBPS_CFG1 (0x02)// +#define MCP_12MHz_100kBPS_CFG2 (0xB6)// +#define MCP_12MHz_100kBPS_CFG3 (0x04)// + +#define MCP_12MHz_95kBPS_CFG1 (0x02)// +#define MCP_12MHz_95kBPS_CFG2 (0xBE)// +#define MCP_12MHz_95kBPS_CFG3 (0x04)// + +#define MCP_12MHz_83k3BPS_CFG1 (0x03)// +#define MCP_12MHz_83k3BPS_CFG2 (0xB5)// +#define MCP_12MHz_83k3BPS_CFG3 (0x03)// + +#define MCP_12MHz_80kBPS_CFG1 (0x04)// +#define MCP_12MHz_80kBPS_CFG2 (0xA4)// +#define MCP_12MHz_80kBPS_CFG3 (0x03)// + +#define MCP_12MHz_50kBPS_CFG1 (0x05)// +#define MCP_12MHz_50kBPS_CFG2 (0xB6)// +#define MCP_12MHz_50kBPS_CFG3 (0x04)// + +#define MCP_12MHz_40kBPS_CFG1 (0x09)// +#define MCP_12MHz_40kBPS_CFG2 (0xA4)// +#define MCP_12MHz_40kBPS_CFG3 (0x03)// + +#define MCP_12MHz_33kBPS_CFG1 (0x0C)// +#define MCP_12MHz_33kBPS_CFG2 (0xA4)// +#define MCP_12MHz_33kBPS_CFG3 (0x02)// + +#define MCP_12MHz_31k25BPS_CFG1 (0x0B)// +#define MCP_12MHz_31k25BPS_CFG2 (0xAC)// +#define MCP_12MHz_31k25BPS_CFG3 (0x03)// + +#define MCP_12MHz_25kBPS_CFG1 (0X0B)// +#define MCP_12MHz_25kBPS_CFG2 (0XB6)// +#define MCP_12MHz_25kBPS_CFG3 (0X04)// + +#define MCP_12MHz_20kBPS_CFG1 (0x0C)// +#define MCP_12MHz_20kBPS_CFG2 (0xBF)// +#define MCP_12MHz_20kBPS_CFG3 (0x05)// + + + +// speed 8M + +#define MCP_8MHz_1000kBPS_CFG1 (0x00) +#define MCP_8MHz_1000kBPS_CFG2 (0x80) +#define MCP_8MHz_1000kBPS_CFG3 (0x00) + +#define MCP_8MHz_800kBPS_CFG1 (0x00) +#define MCP_8MHz_800kBPS_CFG2 (0x80) +#define MCP_8MHz_800kBPS_CFG3 (0x01) + +#define MCP_8MHz_500kBPS_CFG1 (0x00) +#define MCP_8MHz_500kBPS_CFG2 (0x90) +#define MCP_8MHz_500kBPS_CFG3 (0x02) + +#define MCP_8MHz_250kBPS_CFG1 (0x00) +#define MCP_8MHz_250kBPS_CFG2 (0xb1) +#define MCP_8MHz_250kBPS_CFG3 (0x05) + +#define MCP_8MHz_200kBPS_CFG1 (0x00) +#define MCP_8MHz_200kBPS_CFG2 (0xb4) +#define MCP_8MHz_200kBPS_CFG3 (0x06) + +#define MCP_8MHz_125kBPS_CFG1 (0x01) +#define MCP_8MHz_125kBPS_CFG2 (0xb1) +#define MCP_8MHz_125kBPS_CFG3 (0x05) + +#define MCP_8MHz_100kBPS_CFG1 (0x01) +#define MCP_8MHz_100kBPS_CFG2 (0xb4) +#define MCP_8MHz_100kBPS_CFG3 (0x06) + +#define MCP_8MHz_80kBPS_CFG1 (0x01) +#define MCP_8MHz_80kBPS_CFG2 (0xbf) +#define MCP_8MHz_80kBPS_CFG3 (0x07) + +#define MCP_8MHz_50kBPS_CFG1 (0x03) +#define MCP_8MHz_50kBPS_CFG2 (0xb4) +#define MCP_8MHz_50kBPS_CFG3 (0x06) + +#define MCP_8MHz_40kBPS_CFG1 (0x03) +#define MCP_8MHz_40kBPS_CFG2 (0xbf) +#define MCP_8MHz_40kBPS_CFG3 (0x07) + +#define MCP_8MHz_31k25BPS_CFG1 (0x07) +#define MCP_8MHz_31k25BPS_CFG2 (0xa4) +#define MCP_8MHz_31k25BPS_CFG3 (0x04) + +#define MCP_8MHz_20kBPS_CFG1 (0x07) +#define MCP_8MHz_20kBPS_CFG2 (0xbf) +#define MCP_8MHz_20kBPS_CFG3 (0x07) + +#define MCP_8MHz_10kBPS_CFG1 (0x0f) +#define MCP_8MHz_10kBPS_CFG2 (0xbf) +#define MCP_8MHz_10kBPS_CFG3 (0x07) + +#define MCP_8MHz_5kBPS_CFG1 (0x1f) +#define MCP_8MHz_5kBPS_CFG2 (0xbf) +#define MCP_8MHz_5kBPS_CFG3 (0x07) + +#define MCPDEBUG (0) +#define MCPDEBUG_TXBUF (0) +#define MCP_N_TXBUFFERS (3) + +#define MCP_RXBUF_0 (MCP_RXB0SIDH) +#define MCP_RXBUF_1 (MCP_RXB1SIDH) + +#define MCP2515_SELECT() digitalWrite(SPICS, LOW) +#define MCP2515_UNSELECT() digitalWrite(SPICS, HIGH) + +#define MCP2515_OK (0) +#define MCP2515_FAIL (1) +#define MCP_ALLTXBUSY (2) + +#define CANDEBUG 1 + +#define CANUSELOOP 0 + +#define CANSENDTIMEOUT (200) // milliseconds + +#define MCP_PIN_HIZ (0) +#define MCP_PIN_INT (1) +#define MCP_PIN_OUT (2) +#define MCP_PIN_IN (3) + +#define MCP_RX0BF (0) +#define MCP_RX1BF (1) +#define MCP_TX0RTS (2) +#define MCP_TX1RTS (3) +#define MCP_TX2RTS (4) + + +// initial value of gCANAutoProcess + +#define CANAUTOPROCESS (1) +#define CANAUTOON (1) +#define CANAUTOOFF (0) +#define CAN_STDID (0) +#define CAN_EXTID (1) +#define CANDEFAULTIDENT (0x55CC) +#define CANDEFAULTIDENTEXT (CAN_EXTID) + + + + +#define CAN_MAX_CHAR_IN_MESSAGE (8) + +#endif +/********************************************************************************************************* + END FILE +*********************************************************************************************************/ diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/README.md b/README.md new file mode 100644 index 0000000..db1a803 --- /dev/null +++ b/README.md @@ -0,0 +1,187 @@ +
+ +# [JK-BMS To Pylontech CAN Protocol Converter](https://github.com/ArminJo/JK-BMSToPylontechCAN) + +Converts the JK-BMS RS485 data to Pylontech CAN data for inverters which are not compatible with JK-BMS protocol but with Pylontech protocol, like Deye inverters.
+Display of many BMS information and alarms on a locally attached 2004 LCD.
+ +[![Badge License: GPLv3](https://img.shields.io/badge/License-GPLv3-brightgreen.svg)](https://www.gnu.org/licenses/gpl-3.0) +     +[![Badge Version](https://img.shields.io/github/v/release/ArminJo/OpenledRace?include_prereleases&color=yellow&logo=DocuSign&logoColor=white)](https://github.com/ArminJo/JK-BMSToPylontechCAN/releases/latest) +     +[![Badge Commits since latest](https://img.shields.io/github/commits-since/ArminJo/JK-BMSToPylontechCAN/latest?color=yellow)](https://github.com/ArminJo/JK-BMSToPylontechCAN/commits/main) +     +[![Badge Build Status](https://github.com/ArminJo/JK-BMSToPylontechCAN/workflows/TestCompile/badge.svg)](https://github.com/ArminJo/JK-BMSToPylontechCAN/actions) +     +![Badge Hit Counter](https://visitor-badge.laobi.icu/badge?page_id=ArminJo_JK-BMSToPylontechCAN) +
+ +Based on https://github.com/syssi/esphome-jk-bms and https://github.com/maxx-ukoo/jk-bms2pylontech.
+The JK-BMS RS485 data (e.g. at connector GPS) are provided as RS232 TTL with 105200 Bit/s. + +
+
+ +# Features +- Protocol converter. +- Display of BMS information and alarms on a locally attached serial 2004 LCD. +- Beep on Alarm. + +**On a MCP2515 / TJA1050 kit for Arduino you must [replace the assembled 8 MHz crystal with a 16 MHz one](https://www.mittns.de/thread/1340-mcp2515-8mhz-auf-16mhz-upgrade/)** + +
+ +![Overview](https://github.com/ArminJo/JK-BMSToPylontechCAN/blob/main/pictures/BreadboardAndOverviewPage.jpg) + +
+ +| Breadboard detail | Automatic brightness | +| :-: | :-: | +| ![Breadbaoard detail](https://github.com/ArminJo/JK-BMSToPylontechCAN/blob/main/pictures/BreadbaoardDetail.jpg) | ![Automatic brightness](https://github.com/ArminJo/JK-BMSToPylontechCAN/blob/main/pictures/AutomaticBrightness.jpg) | +| Big Info Page | Cell Info Page | +| ![Big Info Page](https://github.com/ArminJo/JK-BMSToPylontechCAN/blob/main/pictures/BigInfoPage.jpg) | ![Cell Info Page](https://github.com/ArminJo/JK-BMSToPylontechCAN/blob/main/pictures/CellInfoPage.jpg) | + +
+ +# Principle of operation +1. A request is sent to the BMS. +2. The BMS reply frame is stored in a buffer and parity and other plausi checks are made. +3. The cell data are converted and enhanced to fill the JKConvertedCellInfoStruct. +4. Other frame data are converted and enhanced to fill the JKComputedDataStruct. +5. The content of the result frame is printed. After reset, all info is printed once, then only dynamic info is printed. +6. The required CAN data is filled in the according PylontechCANFrameInfoStruct. +7. Dynamic data and errors are displayed on the optional 2004 LCD if attached. +8. CAN data is sent. + +
+ +# Compile with the Arduino IDE +Download and extract the repository. In the Arduino IDE open the sketch with File -> Open... and select the JK-BMSToPylontechCAN folder.
+All libraries, especially the modified ones, are included in this project. + +# Libraries used +This program uses the following libraries, which are included in this repository: + +- [SoftwareSerialTX](https://reference.arduino.cc/reference/en/libraries/liquidcrystal-i2c/) for sending Serial to JK-BMS. +- Modified [LiquidCrystal_I2C]() for I2C connected LCD. +- [LCDBigNumbers](https://github.com/ArminJo/LCDBigNumbers) for LCD big number generation. +- [EasyButtonAtInt01](https://github.com/ArminJo/EasyButtonAtInt01) for LCD page switching button. +- [SoftI2CMaster](https://github.com/felias-fogg/SoftI2CMaster) for minimal I2C functions. +- Modified mcp_can_dfs.h file from Seed-Studio [Seeed_Arduino_CAN](https://github.com/Seeed-Studio/Seeed_Arduino_CAN). + +
+ +# Disclaimer +Currently (1.6.2023) the program is tested only with a JK-BMS JK-B2A20S20P and a 10 cell LiIon battery.
+It was not connected to a Deye inverter so far, since the target 16 cell LiFePo battery is stil on its way. + +
+ +# BOM +### Required +- Breadboard. +- Jumper wire. +- Pin header to connect cables to breadboard. +- Shottky diode e.g. BAT 43. +- Arduino Nano. +- 16 (or 20) MHz crystal. +- MCP2515 / TJA1050 kit for Arduino. !!! You must replace the assembled 8 MHz crystal with a 16 MHz one !!! + +### Optional +- 2004 LCD with serial I2C interface adapter. +- LDR for automatic LCD brightness control. +- BC 549C or any type with hFe > 250 for automatic LCD brightness control. + +
+ +### Links: +- https://www.kvaser.com/support/calculators/bit-timing-calculator/ +- https://www.setfirelabs.com/green-energy/pylontech-can-reading-can-replication + +#### If you find this program useful, please give it a star. + +# Sample Serial output +See also [here](https://github.com/ArminJo/tree/main/extras). + +``` +START ../src/JK-BMSToPylontechCAN.cpp +Version 1.0 from May 29 2023 +Serial to JK-BMS started with 115200 bit/s! +CAN started with 500 kbit/s! +If you connect debug pin DEBUG_BUTTON_PIN to ground, additional debug data is printed + +*** BMS INFO *** +Protocol Version Number=1 +Software Version Number=11.XW_S11.26___ +Modify Parameter Password=123456 +# External Temperature Sensors=2 + +*** BATTERY INFO *** +Manufacturer Id=123456789012JK_B2A20S20P +Manufacturer Date=2305 +Device ID String=12345678 +Device Address=1 +Total Battery Capacity[Ah]=30, Low Capacity Alarm Percent=20 +Charging Cycles=0 +Total Charging Cycle Capacity=0 +# Battery Cells=10, Cell Count=10 + +*** VOLTAGE PROTECTION INFO *** +Battery Overvoltage Protection[mV]=42000, Undervoltage=28200 +Cell Overvoltage Protection[mV]=4200, Recovery=4180, Delay[s]=5 +Cell Undervoltage Protection[mV]=2820, Recovery=2850, Delay[s]=5 +Cell Voltage Difference Protection[mV]=300 +Discharging Overcurrent Protection[A]=1, Delay[s]=20 +Charging Overcurrent Protection[A]=1, Delay[s]=30 + +*** TEMPERATURE PROTECTION INFO *** +Power MosFet Temperature Protection=55, Recovery=50 +Sensor1 Temperature Protection=100, Recovery=100 +Sensor1 to Sensor2 Temperature Difference Protection=20 +Charge Overtemperature Protection=80, Discharge=70 +Charge Undertemperature Protection=-5, Recovery=0 +Discharge Undertemperature Protection=-20, Recovery=-10 + +*** MISC INFO *** +Balance Starting Cell Voltage=[mV]3000 +Balance Opening Voltage Difference[mV]=10 + +Current Calibration[mA]=1039 +Sleep Wait Time[s]=10 + +Dedicated Charge Switch Active=0 +Start Current Calibration State=0 +Battery Actual Capacity[Ah]=30 + +*** CELL INFO *** + 1=3967 mV, 2=3965 mV, 3=3965 mV, 4=3965 mV, 5=3965 mV, 6=3967 mV, 7=3968 mV, 8=3968 mV, + 9=3969 mV, 10=3969 mV, +Minimum=3965 mV at cell #2, Maximum=3969 mV at cell #9 +Delta=4 mV, Average=3966 mV + +*** DYNAMIC INFO *** +Total Runtime Minutes=1640 -> 1D 3H20M +Temperature: Power MosFet=26, Sensor 1=44, Sensor 2=57 + +SOC[%]=83 -> Remaining Capacity[Ah]=24 +Battery Voltage[V]=39.66, Current[A]=0.00, Power[W]=0 + +Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, not active + + +*** CELL INFO *** + 1=3967 mV, 2=3965 mV, 3=3965 mV, 4=3965 mV, 5=3965 mV, 6=3967 mV, 7=3968 mV, 8=3967 mV, + 9=3969 mV, 10=3968 mV, +Minimum=3965 mV at cell #2, Maximum=3969 mV at cell #9 +Delta=4 mV, Average=3966 mV + +*** DYNAMIC INFO *** +Total Runtime Minutes=1641 -> 1D 3H21M +Temperature: Power MosFet=26, Sensor 1=44, Sensor 2=57 + +SOC[%]=83 -> Remaining Capacity[Ah]=24 +Battery Voltage[V]=39.66, Current[A]=0.00, Power[W]=0 + +Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, not active + +``` diff --git a/extras/JK-BMS_Debug.log b/extras/JK-BMS_Debug.log new file mode 100644 index 0000000..27971a8 --- /dev/null +++ b/extras/JK-BMS_Debug.log @@ -0,0 +1,148 @@ +START ../src/JK-BMSToPylontechCAN.cpp +Version 1.0 from May 29 2023 +Serial to JK-BMS started with 115200 bit/s! +CAN started with 500 kbit/s! +If you connect debug pin DEBUG_BUTTON_PIN to ground, additional debug data is printed + +Send requestFrame with TxToJKBMS + 0x4E 0x57 0x0 0x13 0x0 0x0 0x0 0x0 0x6 0x3 0x0 0x0 0x0 0x0 0x0 0x0 0x68 0x0 0x0 0x1 0x29 + +273 bytes received +0x00 0x4E 0x57 0x01 0x0F 0x00 0x00 0x00 0x00 0x06 0x00 0x01 +0x0B 0x79 0x1E 0x01 0x0F 0x7F +0x10 0x02 0x0F 0x7D 0x03 0x0F 0x7D 0x04 0x0F 0x7F 0x05 0x0F 0x7F 0x06 0x0F 0x7F 0x07 +0x20 0x0F 0x80 0x08 0x0F 0x7F 0x09 0x0F 0x81 0x0A 0x0F 0x80 +0x2B 0x80 0x00 0x1A 0x81 0x00 +0x30 0x2C 0x82 0x00 0x39 0x83 0x0F 0x7E 0x84 0x00 0x00 0x85 0x53 0x86 0x02 0x87 0x00 +0x40 0x00 0x89 0x00 0x00 0x00 0x00 0x8A 0x00 0x0A 0x8B 0x00 0x00 0x8C 0x00 0x03 0x8E +0x50 0x10 0x68 0x8F 0x0B 0x04 0x90 0x10 0x68 0x91 0x10 0x54 0x92 0x00 0x05 0x93 0x0B +0x60 0x04 0x94 0x0B 0x22 0x95 0x00 0x05 0x96 0x01 0x2C 0x97 0x00 0x01 0x98 0x00 0x14 +0x70 0x99 0x00 0x01 0x9A 0x00 0x1E 0x9B 0x0B 0xB8 0x9C 0x00 0x0A 0x9D 0x01 0x9E 0x00 +0x80 0x37 0x9F 0x00 0x32 0xA0 0x00 0x64 0xA1 0x00 0x64 0xA2 0x00 0x14 0xA3 0x00 0x50 +0x90 0xA4 0x00 0x46 0xA5 0xFF 0xFB 0xA6 0x00 0x00 0xA7 0xFF 0xEC 0xA8 0xFF 0xF6 0xA9 +0xA0 0x0A 0xAA 0x00 0x00 0x00 0x1E 0xAB 0x01 0xAC 0x01 0xAD 0x04 0x0F 0xAE 0x01 0xAF +0xB0 0x01 0xB0 0x00 0x0A 0xB1 0x14 0xB2 0x31 0x32 0x33 0x34 0x35 0x36 0x00 0x00 0x00 +0xC0 0x00 0xB3 0x00 0xB4 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0xB5 0x32 0x33 0x30 +0xD0 0x35 0xB6 0x00 0x00 0x06 0x68 0xB7 0x31 0x31 0x2E 0x58 0x57 0x5F 0x53 0x31 0x31 +0xE0 0x2E 0x32 0x36 0x5F 0x5F 0x5F 0xB8 0x00 0xB9 0x00 0x00 0x00 0x1E 0xBA 0x31 0x32 +0xF0 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x4A 0x4B 0x5F 0x42 0x32 0x41 +0x100 0x32 0x30 0x53 0x32 0x30 0x50 0xC0 0x01 +0x108 0x00 0x00 0x00 0x00 0x68 0x00 0x00 0x45 +0x110 0x66 + +*** BMS INFO *** +Protocol Version Number=1 +Software Version Number=11.XW_S11.26___ +Modify Parameter Password=123456 +# External Temperature Sensors=2 + +*** BATTERY INFO *** +Manufacturer Id=123456789012JK_B2A20S20P +Manufacturer Date=2305 +Device ID String=12345678 +Device Address=1 +Total Battery Capacity[Ah]=30, Low Capacity Alarm Percent=20 +Charging Cycles=0 +Total Charging Cycle Capacity=0 +# Battery Cells=10, Cell Count=10 + +*** VOLTAGE PROTECTION INFO *** +Battery Overvoltage Protection[mV]=42000, Undervoltage=28200 +Cell Overvoltage Protection[mV]=4200, Recovery=4180, Delay[s]=5 +Cell Undervoltage Protection[mV]=2820, Recovery=2850, Delay[s]=5 +Cell Voltage Difference Protection[mV]=300 +Discharging Overcurrent Protection[A]=1, Delay[s]=20 +Charging Overcurrent Protection[A]=1, Delay[s]=30 + +*** TEMPERATURE PROTECTION INFO *** +Power MosFet Temperature Protection=55, Recovery=50 +Sensor1 Temperature Protection=100, Recovery=100 +Sensor1 to Sensor2 Temperature Difference Protection=20 +Charge Overtemperature Protection=80, Discharge=70 +Charge Undertemperature Protection=-5, Recovery=0 +Discharge Undertemperature Protection=-20, Recovery=-10 + +*** MISC INFO *** +Balance Starting Cell Voltage=[mV]3000 +Balance Opening Voltage Difference[mV]=10 + +Current Calibration[mA]=1039 +Sleep Wait Time[s]=10 + +Dedicated Charge Switch Active=0 +Start Current Calibration State=0 +Battery Actual Capacity[Ah]=30 + +*** CELL INFO *** + 1=3967 mV, 2=3965 mV, 3=3965 mV, 4=3967 mV, 5=3967 mV, 6=3967 mV, 7=3968 mV, 8=3967 mV, + 9=3969 mV, 10=3968 mV, +Minimum=3965 mV at cell #2, Maximum=3969 mV at cell #9 +Delta=4 mV, Average=3967 mV + +*** DYNAMIC INFO *** +Total Runtime Minutes=1640 -> 1D 3H20M +Temperature: Power MosFet=26, Sensor 1=44, Sensor 2=57 + +SOC[%]=83 -> Remaining Capacity[Ah]=24 +Battery Voltage[V]=39.66, Current[A]=0.00, Power[W]=0 + +Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, not active + + +Send CAN +CANId=0x351, FrameLength=8, Data=0xA4, 0x1, 0xA, 0x0, 0xA, 0x0, 0x1A, 0x1 +CANId=0x355, FrameLength=4, Data=0x53, 0x0, 0x5F, 0x0 +CANId=0x356, FrameLength=6, Data=0x8C, 0x1, 0x0, 0x0, 0x3A, 0x2 +CANId=0x35E, FrameLength=8, Data=0x50, 0x59, 0x4C, 0x4F, 0x4E, 0x20, 0x20, 0x20 +CANId=0x35C, FrameLength=2, Data=0xC0, 0x0 +CANId=0x305, FrameLength=8, Data=0x21, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 +CANId=0x359, FrameLength=7, Data=0x0, 0x0, 0x0, 0x0, 0x1, 0x50, 0x4E +Send requestFrame with TxToJKBMS + 0x4E 0x57 0x0 0x13 0x0 0x0 0x0 0x0 0x6 0x3 0x0 0x0 0x0 0x0 0x0 0x0 0x68 0x0 0x0 0x1 0x29 + +273 bytes received +0x00 0x4E 0x57 0x01 0x0F 0x00 0x00 0x00 0x00 0x06 0x00 0x01 +0x0B 0x79 0x1E 0x01 0x0F 0x7F +0x10 0x02 0x0F 0x7D 0x03 0x0F 0x7F 0x04 0x0F 0x7F 0x05 0x0F 0x7F 0x06 0x0F 0x7F 0x07 +0x20 0x0F 0x80 0x08 0x0F 0x7F 0x09 0x0F 0x80 0x0A 0x0F 0x81 +0x2B 0x80 0x00 0x1A 0x81 0x00 +0x30 0x2C 0x82 0x00 0x39 0x83 0x0F 0x7E 0x84 0x00 0x00 0x85 0x53 0x86 0x02 0x87 0x00 +0x40 0x00 0x89 0x00 0x00 0x00 0x00 0x8A 0x00 0x0A 0x8B 0x00 0x00 0x8C 0x00 0x03 0x8E +0x50 0x10 0x68 0x8F 0x0B 0x04 0x90 0x10 0x68 0x91 0x10 0x54 0x92 0x00 0x05 0x93 0x0B +0x60 0x04 0x94 0x0B 0x22 0x95 0x00 0x05 0x96 0x01 0x2C 0x97 0x00 0x01 0x98 0x00 0x14 +0x70 0x99 0x00 0x01 0x9A 0x00 0x1E 0x9B 0x0B 0xB8 0x9C 0x00 0x0A 0x9D 0x01 0x9E 0x00 +0x80 0x37 0x9F 0x00 0x32 0xA0 0x00 0x64 0xA1 0x00 0x64 0xA2 0x00 0x14 0xA3 0x00 0x50 +0x90 0xA4 0x00 0x46 0xA5 0xFF 0xFB 0xA6 0x00 0x00 0xA7 0xFF 0xEC 0xA8 0xFF 0xF6 0xA9 +0xA0 0x0A 0xAA 0x00 0x00 0x00 0x1E 0xAB 0x01 0xAC 0x01 0xAD 0x04 0x0F 0xAE 0x01 0xAF +0xB0 0x01 0xB0 0x00 0x0A 0xB1 0x14 0xB2 0x31 0x32 0x33 0x34 0x35 0x36 0x00 0x00 0x00 +0xC0 0x00 0xB3 0x00 0xB4 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0xB5 0x32 0x33 0x30 +0xD0 0x35 0xB6 0x00 0x00 0x06 0x68 0xB7 0x31 0x31 0x2E 0x58 0x57 0x5F 0x53 0x31 0x31 +0xE0 0x2E 0x32 0x36 0x5F 0x5F 0x5F 0xB8 0x00 0xB9 0x00 0x00 0x00 0x1E 0xBA 0x31 0x32 +0xF0 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x30 0x31 0x32 0x4A 0x4B 0x5F 0x42 0x32 0x41 +0x100 0x32 0x30 0x53 0x32 0x30 0x50 0xC0 0x01 +0x108 0x00 0x00 0x00 0x00 0x68 0x00 0x00 0x45 +0x110 0x68 + +*** CELL INFO *** + 1=3967 mV, 2=3965 mV, 3=3967 mV, 4=3967 mV, 5=3967 mV, 6=3967 mV, 7=3968 mV, 8=3967 mV, + 9=3968 mV, 10=3969 mV, +Minimum=3965 mV at cell #2, Maximum=3969 mV at cell #10 +Delta=4 mV, Average=3967 mV + +*** DYNAMIC INFO *** +Temperature: Power MosFet=26, Sensor 1=44, Sensor 2=57 + +SOC[%]=83 -> Remaining Capacity[Ah]=24 +Battery Voltage[V]=39.66, Current[A]=0.00, Power[W]=0 + +Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, not active + + +Send CAN +CANId=0x351, FrameLength=8, Data=0xA4, 0x1, 0xA, 0x0, 0xA, 0x0, 0x1A, 0x1 +CANId=0x355, FrameLength=4, Data=0x53, 0x0, 0x5F, 0x0 +CANId=0x356, FrameLength=6, Data=0x8C, 0x1, 0x0, 0x0, 0x3A, 0x2 +CANId=0x35E, FrameLength=8, Data=0x50, 0x59, 0x4C, 0x4F, 0x4E, 0x20, 0x20, 0x20 +CANId=0x35C, FrameLength=2, Data=0xC0, 0x0 +CANId=0x305, FrameLength=8, Data=0x21, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 +CANId=0x359, FrameLength=7, Data=0x0, 0x0, 0x0, 0x0, 0x1, 0x50, 0x4E diff --git a/extras/JK-BMS_NoDebug.log b/extras/JK-BMS_NoDebug.log new file mode 100644 index 0000000..ff4e355 --- /dev/null +++ b/extras/JK-BMS_NoDebug.log @@ -0,0 +1,96 @@ +START ../src/JK-BMSToPylontechCAN.cpp +Version 1.0 from May 29 2023 +Serial to JK-BMS started with 115200 bit/s! +CAN started with 500 kbit/s! +If you connect debug pin DEBUG_BUTTON_PIN to ground, additional debug data is printed + +*** BMS INFO *** +Protocol Version Number=1 +Software Version Number=11.XW_S11.26___ +Modify Parameter Password=123456 +# External Temperature Sensors=2 + +*** BATTERY INFO *** +Manufacturer Id=123456789012JK_B2A20S20P +Manufacturer Date=2305 +Device ID String=12345678 +Device Address=1 +Total Battery Capacity[Ah]=30, Low Capacity Alarm Percent=20 +Charging Cycles=0 +Total Charging Cycle Capacity=0 +# Battery Cells=10, Cell Count=10 + +*** VOLTAGE PROTECTION INFO *** +Battery Overvoltage Protection[mV]=42000, Undervoltage=28200 +Cell Overvoltage Protection[mV]=4200, Recovery=4180, Delay[s]=5 +Cell Undervoltage Protection[mV]=2820, Recovery=2850, Delay[s]=5 +Cell Voltage Difference Protection[mV]=300 +Discharging Overcurrent Protection[A]=1, Delay[s]=20 +Charging Overcurrent Protection[A]=1, Delay[s]=30 + +*** TEMPERATURE PROTECTION INFO *** +Power MosFet Temperature Protection=55, Recovery=50 +Sensor1 Temperature Protection=100, Recovery=100 +Sensor1 to Sensor2 Temperature Difference Protection=20 +Charge Overtemperature Protection=80, Discharge=70 +Charge Undertemperature Protection=-5, Recovery=0 +Discharge Undertemperature Protection=-20, Recovery=-10 + +*** MISC INFO *** +Balance Starting Cell Voltage=[mV]3000 +Balance Opening Voltage Difference[mV]=10 + +Current Calibration[mA]=1039 +Sleep Wait Time[s]=10 + +Dedicated Charge Switch Active=0 +Start Current Calibration State=0 +Battery Actual Capacity[Ah]=30 + +*** CELL INFO *** + 1=3967 mV, 2=3965 mV, 3=3965 mV, 4=3965 mV, 5=3965 mV, 6=3967 mV, 7=3968 mV, 8=3968 mV, + 9=3969 mV, 10=3969 mV, +Minimum=3965 mV at cell #2, Maximum=3969 mV at cell #9 +Delta=4 mV, Average=3966 mV + +*** DYNAMIC INFO *** +Total Runtime Minutes=1640 -> 1D 3H20M +Temperature: Power MosFet=26, Sensor 1=44, Sensor 2=57 + +SOC[%]=83 -> Remaining Capacity[Ah]=24 +Battery Voltage[V]=39.66, Current[A]=0.00, Power[W]=0 + +Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, not active + + +*** CELL INFO *** + 1=3967 mV, 2=3965 mV, 3=3965 mV, 4=3965 mV, 5=3965 mV, 6=3967 mV, 7=3968 mV, 8=3967 mV, + 9=3969 mV, 10=3968 mV, +Minimum=3965 mV at cell #2, Maximum=3969 mV at cell #9 +Delta=4 mV, Average=3966 mV + +*** DYNAMIC INFO *** +Total Runtime Minutes=1641 -> 1D 3H21M +Temperature: Power MosFet=26, Sensor 1=44, Sensor 2=57 + +SOC[%]=83 -> Remaining Capacity[Ah]=24 +Battery Voltage[V]=39.66, Current[A]=0.00, Power[W]=0 + +Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, not active + + +*** CELL INFO *** + 1=3967 mV, 2=3965 mV, 3=3965 mV, 4=3965 mV, 5=3965 mV, 6=3967 mV, 7=3968 mV, 8=3967 mV, + 9=3969 mV, 10=3968 mV, +Minimum=3965 mV at cell #2, Maximum=3969 mV at cell #9 +Delta=4 mV, Average=3966 mV + +*** DYNAMIC INFO *** +Temperature: Power MosFet=26, Sensor 1=44, Sensor 2=57 + +SOC[%]=83 -> Remaining Capacity[Ah]=24 +Battery Voltage[V]=39.66, Current[A]=0.00, Power[W]=0 + +Charging MosFet enabled, active | Discharging MosFet enabled, active | Balancing enabled, not active + + diff --git a/pictures/AutomaticBrightness.jpg b/pictures/AutomaticBrightness.jpg new file mode 100644 index 0000000..6f6137c Binary files /dev/null and b/pictures/AutomaticBrightness.jpg differ diff --git a/pictures/BigInfoPage.jpg b/pictures/BigInfoPage.jpg new file mode 100644 index 0000000..2bc8a35 Binary files /dev/null and b/pictures/BigInfoPage.jpg differ diff --git a/pictures/BreadbaoardDetail.jpg b/pictures/BreadbaoardDetail.jpg new file mode 100644 index 0000000..65451e2 Binary files /dev/null and b/pictures/BreadbaoardDetail.jpg differ diff --git a/pictures/BreadboardAndOverviewPage.jpg b/pictures/BreadboardAndOverviewPage.jpg new file mode 100644 index 0000000..bd24057 Binary files /dev/null and b/pictures/BreadboardAndOverviewPage.jpg differ diff --git a/pictures/CellInfoPage.jpg b/pictures/CellInfoPage.jpg new file mode 100644 index 0000000..99c11b2 Binary files /dev/null and b/pictures/CellInfoPage.jpg differ