From 5de2af3bdfeefe05e055119526ff2c84bdd054ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20L=C3=B6w?= Date: Tue, 4 Feb 2025 00:11:14 +0100 Subject: [PATCH 1/8] :sparkles: :construction: initial version of talking to DALY BMS via RS485 --- Software/Software.ino | 6 +- Software/USER_SETTINGS.h | 1 + Software/src/battery/BATTERIES.h | 14 +- Software/src/battery/DALY-BMS.cpp | 148 ++++++++++++++++++ Software/src/battery/DALY-BMS.h | 26 +++ .../src/communication/rs485/comm_rs485.cpp | 4 +- 6 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 Software/src/battery/DALY-BMS.cpp create mode 100644 Software/src/battery/DALY-BMS.h diff --git a/Software/Software.ino b/Software/Software.ino index 619a01535..b065cf726 100644 --- a/Software/Software.ino +++ b/Software/Software.ino @@ -210,7 +210,7 @@ void core_loop(void* task_time_us) { // Input, Runs as fast as possible receive_can(); // Receive CAN messages -#ifdef RS485_INVERTER_SELECTED +#if defined(RS485_INVERTER_SELECTED) || defined(RS485_BATTERY_SELECTED) receive_RS485(); // Process serial2 RS485 interface #endif // RS485_INVERTER_SELECTED #if defined(SERIAL_LINK_RECEIVER) || defined(SERIAL_LINK_TRANSMITTER) @@ -255,6 +255,10 @@ void core_loop(void* task_time_us) { // Output transmit_can(); // Send CAN messages to all components +#ifdef RS485_BATTERY_SELECTED + transmit_rs485(); +#endif // RS485_BATTERY_SELECTED + END_TIME_MEASUREMENT_MAX(cantx, datalayer.system.status.time_cantx_us); END_TIME_MEASUREMENT_MAX(all, datalayer.system.status.core_task_10s_max_us); #ifdef FUNCTION_TIME_MEASUREMENT diff --git a/Software/USER_SETTINGS.h b/Software/USER_SETTINGS.h index 5b56850c2..052e14266 100644 --- a/Software/USER_SETTINGS.h +++ b/Software/USER_SETTINGS.h @@ -26,6 +26,7 @@ //#define MG_5_BATTERY //#define NISSAN_LEAF_BATTERY //#define PYLON_BATTERY +//#define DALY_BMS //#define RJXZS_BMS //#define RANGE_ROVER_PHEV_BATTERY //#define RENAULT_KANGOO_BATTERY diff --git a/Software/src/battery/BATTERIES.h b/Software/src/battery/BATTERIES.h index 5ad2417f8..27b684bf6 100644 --- a/Software/src/battery/BATTERIES.h +++ b/Software/src/battery/BATTERIES.h @@ -86,6 +86,10 @@ void setup_can_shunt(); #include "PYLON-BATTERY.h" #endif +#ifdef DALY_BMS +#include "DALY-BMS.h" +#endif + #ifdef RJXZS_BMS #include "RJXZS-BMS.h" #endif @@ -131,10 +135,16 @@ void setup_can_shunt(); #include "SERIAL-LINK-RECEIVER-FROM-BATTERY.h" #endif -void handle_incoming_can_frame_battery(CAN_frame rx_frame); +void setup_battery(void); void update_values_battery(); + +#ifdef RS485_BATTERY_SELECTED +void transmit_rs485(); +void receive_RS485(); +#else +void handle_incoming_can_frame_battery(CAN_frame rx_frame); void transmit_can_battery(); -void setup_battery(void); +#endif #ifdef DOUBLE_BATTERY void update_values_battery2(); diff --git a/Software/src/battery/DALY-BMS.cpp b/Software/src/battery/DALY-BMS.cpp new file mode 100644 index 000000000..77d4e6e64 --- /dev/null +++ b/Software/src/battery/DALY-BMS.cpp @@ -0,0 +1,148 @@ +#include "DALY-BMS.h" +#include +#include "../include.h" +#include "RJXZS-BMS.h" +#ifdef DALY_BMS +#include "../datalayer/datalayer.h" +#include "../devboard/utils/events.h" +#include "RENAULT-TWIZY.h" + +/* Do not change code below unless you are sure what you are doing */ + +static int16_t temperature_min = 0; +static int16_t temperature_max = 0; +static int16_t current_dA = 0; +static uint16_t voltage_dV = 0; +static uint16_t remaining_capacity_Ah = 0; +static uint16_t cellvoltages_mV[48] = {0}; +static uint16_t cellvoltage_min = 0; +static uint16_t cellvoltage_max = 0; +static uint16_t SOC = 0; + +void update_values_battery() { + datalayer.battery.status.real_soc = SOC; + datalayer.battery.status.voltage_dV = voltage_dV; //value is *10 (3700 = 370.0) + datalayer.battery.status.current_dA = current_dA; //value is *10 (150 = 15.0) + datalayer.battery.status.remaining_capacity_Wh = (remaining_capacity_Ah * DESIGN_PACK_VOLTAGE_DB) / 10; + + datalayer.battery.status.max_charge_power_W = MAX_CHARGE_POWER_ALLOWED_W; + datalayer.battery.status.max_discharge_power_W = MAX_DISCHARGE_POWER_ALLOWED_W; + + memcpy(datalayer.battery.status.cell_voltages_mV, cellvoltages_mV, sizeof(cellvoltages_mV)); + datalayer.battery.status.cell_min_voltage_mV = cellvoltage_min; + datalayer.battery.status.cell_max_voltage_mV = cellvoltage_max; + + datalayer.battery.status.temperature_min_dC = temperature_min; + datalayer.battery.status.temperature_max_dC = temperature_max; +} + +void setup_battery(void) { // Performs one time setup at startup + strncpy(datalayer.system.info.battery_protocol, "DALY RS485", 63); + datalayer.system.info.battery_protocol[63] = '\0'; + datalayer.battery.info.number_of_cells = CELL_COUNT; + datalayer.battery.info.max_design_voltage_dV = MAX_PACK_VOLTAGE_DV; + datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV; + datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV; + datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV; +} + +uint8_t calculate_checksum(uint8_t buff[12]) { + uint8_t check = 0; + for (uint8_t i = 0; i < 12; i++) { + check += buff[i]; + } + return check; +} + +uint16_t decode_uint16be(uint8_t data[8], uint8_t offset) { + uint16_t upper = data[offset]; + uint16_t lower = data[offset + 1]; + return (upper << 8) | lower; +} +int16_t decode_int16be(uint8_t data[8], uint8_t offset) { + int16_t upper = data[offset]; + int16_t lower = data[offset + 1]; + return (upper << 8) | lower; +} +uint32_t decode_uint32be(uint8_t data[8], uint8_t offset) { + return (((uint32_t)data[offset]) << 24) | (((uint32_t)data[offset + 1]) << 16) | (((uint32_t)data[offset + 2]) << 8) | + ((uint32_t)data[offset + 3]); +} + +void decode_packet(uint8_t data[8]) { + switch (buff[2]) { + case 0x90: + voltage_dV = decode_uint16be(data, 0); + current_dA = decode_int16be(data, 4) - 30000; + SOC = decode_uint16be(data, 6) * 10; + break; + case 0x91: + cellvoltage_max = decode_uint16be(data, 0); + cellvoltage_min = decode_uint16be(data, 3); + break; + case 0x92: + temperature_max = decode_int16be(data, 0) - 40; + temperature_min = decode_int16be(data, 2) - 40; + break; + case 0x93: + remaining_capacity_Ah = decode_uint32be(data, 4); + break; + case 0x94: + break; + case 0x95: + if (data[0] > 0 && data[0] <= 16) { + uint8_t frame_index = (data[0] - 1) * 3; + cellvoltages_mV[frame_index + 0] = decode_uint16be(data, 1); + cellvoltages_mV[frame_index + 1] = decode_uint16be(data, 3); + cellvoltages_mV[frame_index + 2] = decode_uint16be(data, 5); + } + break; + case 0x96: + break; + case 0x97: + break; + case 0x98: + break; + } +} + +void transmit_rs485() { + static uint32_t lastSend = 0; + static uint8_t nextCommand = 0x90; + + if (millis() - lastSend > 10) { + uint8_t tx_buff[13] = {0}; + tx_buff[0] = 0xA5; + tx_buff[1] = 0x80; + tx_buff[2] = nextCommand; + tx_buff[3] = 8; + tx_buff[12] = calculate_checksum(tx_buff); + + Serial2.write(tx_buff, 13); + lastSend = millis(); + } +} + +void receive_RS485() { + static uint8_t recv_buff[13] = {0}; + static uint8_t recv_len = 0; + + while (Serial2.available()) { + recv_buff[recv_len] = Serial2.read(); + recv_len++; + + if (recv_len > 0 && recv_buff[0] != 0xA5 || recv_len > 1 && recv_buff[1] != 0x01 || + recv_len > 2 && (recv_buff[2] < 0x90 || recv_buff[2] > 0x98) || recv_len > 3 && recv_buff[3] != 8 || + recv_len > 12 && recv_buff[12] != calculate_checksum(recv_buff)) { + + recv_len = 0; + } + + if (recv_len > 12) { + decode_packet(&recv_buff[4]); + recv_len = 0; + } + } +} + +#endif diff --git a/Software/src/battery/DALY-BMS.h b/Software/src/battery/DALY-BMS.h new file mode 100644 index 000000000..255234ae4 --- /dev/null +++ b/Software/src/battery/DALY-BMS.h @@ -0,0 +1,26 @@ +#ifndef DALY_BMS_H +#define DALY_BMS_H +#include +#include "../include.h" + +/* Tweak these according to your battery build */ +#define CELL_COUNT 14 +#define DESIGN_PACK_VOLTAGE_DB 528 //528 = 52.8V +#define MAX_PACK_VOLTAGE_DV 588 //588 = 58.8V +#define MIN_PACK_VOLTAGE_DV 518 //518 = 51.8V +#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value +#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value +#define MAX_CELL_DEVIATION_MV 250 +#define MAX_DISCHARGE_POWER_ALLOWED_W 1800 +#define MAX_CHARGE_POWER_ALLOWED_W 1800 +#define MAX_CHARGE_POWER_WHEN_TOPBALANCING_W 50 +#define RAMPDOWN_SOC 9000 // (90.00) SOC% to start ramping down from max charge power towards 0 at 100.00% + +/* Do not modify any rows below*/ +#define BATTERY_SELECTED +#define RS485_BATTERY_SELECTED + +void setup_battery(void); +void receive_RS485(void); + +#endif diff --git a/Software/src/communication/rs485/comm_rs485.cpp b/Software/src/communication/rs485/comm_rs485.cpp index feec33aa5..57d5bd8c9 100644 --- a/Software/src/communication/rs485/comm_rs485.cpp +++ b/Software/src/communication/rs485/comm_rs485.cpp @@ -29,9 +29,9 @@ void init_rs485() { pinMode(PIN_5V_EN, OUTPUT); digitalWrite(PIN_5V_EN, HIGH); #endif // PIN_5V_EN -#ifdef RS485_INVERTER_SELECTED +#if defined(RS485_INVERTER_SELECTED) || defined(RS485_BATTERY_SELECTED) Serial2.begin(57600, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN); -#endif // RS485_INVERTER_SELECTED +#endif // RS485_INVERTER_SELECTED || RS485_BATTERY_SELECTED #ifdef MODBUS_INVERTER_SELECTED #ifdef BYD_MODBUS // Init Static data to the RTU Modbus From 1f5bcde8aaea979d0b0f0065c9945ce056c02b09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20L=C3=B6w?= Date: Tue, 4 Feb 2025 00:32:40 +0100 Subject: [PATCH 2/8] :bug: fix compilation bugs with daly bms --- Software/src/battery/DALY-BMS.cpp | 6 +++--- Software/src/battery/DALY-BMS.h | 8 -------- Software/src/communication/can/comm_can.cpp | 4 ++++ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/Software/src/battery/DALY-BMS.cpp b/Software/src/battery/DALY-BMS.cpp index 77d4e6e64..f81ee01f5 100644 --- a/Software/src/battery/DALY-BMS.cpp +++ b/Software/src/battery/DALY-BMS.cpp @@ -69,8 +69,8 @@ uint32_t decode_uint32be(uint8_t data[8], uint8_t offset) { ((uint32_t)data[offset + 3]); } -void decode_packet(uint8_t data[8]) { - switch (buff[2]) { +void decode_packet(uint8_t command, uint8_t data[8]) { + switch (command) { case 0x90: voltage_dV = decode_uint16be(data, 0); current_dA = decode_int16be(data, 4) - 30000; @@ -139,7 +139,7 @@ void receive_RS485() { } if (recv_len > 12) { - decode_packet(&recv_buff[4]); + decode_packet(recv_buff[2], &recv_buff[4]); recv_len = 0; } } diff --git a/Software/src/battery/DALY-BMS.h b/Software/src/battery/DALY-BMS.h index 255234ae4..7d00490a3 100644 --- a/Software/src/battery/DALY-BMS.h +++ b/Software/src/battery/DALY-BMS.h @@ -1,7 +1,5 @@ #ifndef DALY_BMS_H #define DALY_BMS_H -#include -#include "../include.h" /* Tweak these according to your battery build */ #define CELL_COUNT 14 @@ -10,17 +8,11 @@ #define MIN_PACK_VOLTAGE_DV 518 //518 = 51.8V #define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value #define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value -#define MAX_CELL_DEVIATION_MV 250 #define MAX_DISCHARGE_POWER_ALLOWED_W 1800 #define MAX_CHARGE_POWER_ALLOWED_W 1800 -#define MAX_CHARGE_POWER_WHEN_TOPBALANCING_W 50 -#define RAMPDOWN_SOC 9000 // (90.00) SOC% to start ramping down from max charge power towards 0 at 100.00% /* Do not modify any rows below*/ #define BATTERY_SELECTED #define RS485_BATTERY_SELECTED -void setup_battery(void); -void receive_RS485(void); - #endif diff --git a/Software/src/communication/can/comm_can.cpp b/Software/src/communication/can/comm_can.cpp index 880d7b5e8..264b237aa 100644 --- a/Software/src/communication/can/comm_can.cpp +++ b/Software/src/communication/can/comm_can.cpp @@ -109,7 +109,9 @@ void transmit_can() { return; //Global block of CAN messages } +#ifndef RS485_BATTERY_SELECTED transmit_can_battery(); +#endif #ifdef CAN_INVERTER_SELECTED transmit_can_inverter(); @@ -302,7 +304,9 @@ void map_can_frame_to_variable(CAN_frame* rx_frame, int interface) { #endif if (interface == can_config.battery) { +#ifndef RS485_BATTERY_SELECTED handle_incoming_can_frame_battery(*rx_frame); +#endif #ifdef CHADEMO_BATTERY ISA_handleFrame(rx_frame); #endif From 25edffb12513ad2cac10db9afa4816e561c3d841 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20L=C3=B6w?= Date: Fri, 7 Feb 2025 19:44:29 +0100 Subject: [PATCH 3/8] :bug: fix daly bms data conversion, baud rate and packet format after successfull real world tests --- Software/src/battery/DALY-BMS.cpp | 70 ++++++++++++++----- Software/src/battery/DALY-BMS.h | 15 ++-- .../src/communication/rs485/comm_rs485.cpp | 2 +- Software/src/inverter/KOSTAL-RS485.h | 1 + 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/Software/src/battery/DALY-BMS.cpp b/Software/src/battery/DALY-BMS.cpp index f81ee01f5..240e94086 100644 --- a/Software/src/battery/DALY-BMS.cpp +++ b/Software/src/battery/DALY-BMS.cpp @@ -9,31 +9,32 @@ /* Do not change code below unless you are sure what you are doing */ -static int16_t temperature_min = 0; -static int16_t temperature_max = 0; +static uint32_t lastPacket = 0; +static int16_t temperature_min_dC = 0; +static int16_t temperature_max_dC = 0; static int16_t current_dA = 0; static uint16_t voltage_dV = 0; -static uint16_t remaining_capacity_Ah = 0; +static uint32_t remaining_capacity_mAh = 0; static uint16_t cellvoltages_mV[48] = {0}; -static uint16_t cellvoltage_min = 0; -static uint16_t cellvoltage_max = 0; +static uint16_t cellvoltage_min_mV = 0; +static uint16_t cellvoltage_max_mV = 0; static uint16_t SOC = 0; void update_values_battery() { datalayer.battery.status.real_soc = SOC; datalayer.battery.status.voltage_dV = voltage_dV; //value is *10 (3700 = 370.0) datalayer.battery.status.current_dA = current_dA; //value is *10 (150 = 15.0) - datalayer.battery.status.remaining_capacity_Wh = (remaining_capacity_Ah * DESIGN_PACK_VOLTAGE_DB) / 10; + datalayer.battery.status.remaining_capacity_Wh = (remaining_capacity_mAh * (uint32_t)voltage_dV) / 10000; - datalayer.battery.status.max_charge_power_W = MAX_CHARGE_POWER_ALLOWED_W; - datalayer.battery.status.max_discharge_power_W = MAX_DISCHARGE_POWER_ALLOWED_W; + datalayer.battery.status.max_charge_power_W = (MAX_CHARGE_AMPS * voltage_dV) / 10; + datalayer.battery.status.max_discharge_power_W = (MAX_DISCHARGE_AMPS * voltage_dV) / 10; memcpy(datalayer.battery.status.cell_voltages_mV, cellvoltages_mV, sizeof(cellvoltages_mV)); - datalayer.battery.status.cell_min_voltage_mV = cellvoltage_min; - datalayer.battery.status.cell_max_voltage_mV = cellvoltage_max; + datalayer.battery.status.cell_min_voltage_mV = cellvoltage_min_mV; + datalayer.battery.status.cell_max_voltage_mV = cellvoltage_max_mV; - datalayer.battery.status.temperature_min_dC = temperature_min; - datalayer.battery.status.temperature_max_dC = temperature_max; + datalayer.battery.status.temperature_min_dC = temperature_min_dC; + datalayer.battery.status.temperature_max_dC = temperature_max_dC; } void setup_battery(void) { // Performs one time setup at startup @@ -44,6 +45,7 @@ void setup_battery(void) { // Performs one time setup at startup datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV; datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV; datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV; + datalayer.battery.info.total_capacity_Wh = PACK_CAPACITY_AH; } uint8_t calculate_checksum(uint8_t buff[12]) { @@ -69,7 +71,22 @@ uint32_t decode_uint32be(uint8_t data[8], uint8_t offset) { ((uint32_t)data[offset + 3]); } +#ifdef DEBUG_VIA_USB +void dump_buff(const char* msg, uint8_t* buff, uint8_t len) { + Serial.print("[DALY-BMS] "); + Serial.print(msg); + for (int i = 0; i < len; i++) { + Serial.print(buff[i] >> 4, HEX); + Serial.print(buff[i] & 0xf, HEX); + Serial.print(" "); + } + Serial.println(); +} +#endif + void decode_packet(uint8_t command, uint8_t data[8]) { + datalayer.battery.status.CAN_battery_still_alive = CAN_STILL_ALIVE; + switch (command) { case 0x90: voltage_dV = decode_uint16be(data, 0); @@ -77,15 +94,15 @@ void decode_packet(uint8_t command, uint8_t data[8]) { SOC = decode_uint16be(data, 6) * 10; break; case 0x91: - cellvoltage_max = decode_uint16be(data, 0); - cellvoltage_min = decode_uint16be(data, 3); + cellvoltage_max_mV = decode_uint16be(data, 0); + cellvoltage_min_mV = decode_uint16be(data, 3); break; case 0x92: - temperature_max = decode_int16be(data, 0) - 40; - temperature_min = decode_int16be(data, 2) - 40; + temperature_max_dC = (data[0] - 40) * 10; + temperature_min_dC = (data[1] - 40) * 10; break; case 0x93: - remaining_capacity_Ah = decode_uint32be(data, 4); + remaining_capacity_mAh = decode_uint32be(data, 4); break; case 0x94: break; @@ -110,16 +127,24 @@ void transmit_rs485() { static uint32_t lastSend = 0; static uint8_t nextCommand = 0x90; - if (millis() - lastSend > 10) { + if (millis() - lastSend > 500) { uint8_t tx_buff[13] = {0}; tx_buff[0] = 0xA5; - tx_buff[1] = 0x80; + tx_buff[1] = 0x40; tx_buff[2] = nextCommand; tx_buff[3] = 8; tx_buff[12] = calculate_checksum(tx_buff); +#ifdef DEBUG_VIA_USB + dump_buff("transmitting: ", tx_buff, 13); +#endif + Serial2.write(tx_buff, 13); lastSend = millis(); + + nextCommand++; + if (nextCommand > 0x95) + nextCommand = 0x90; } } @@ -129,16 +154,23 @@ void receive_RS485() { while (Serial2.available()) { recv_buff[recv_len] = Serial2.read(); + recv_len++; if (recv_len > 0 && recv_buff[0] != 0xA5 || recv_len > 1 && recv_buff[1] != 0x01 || recv_len > 2 && (recv_buff[2] < 0x90 || recv_buff[2] > 0x98) || recv_len > 3 && recv_buff[3] != 8 || recv_len > 12 && recv_buff[12] != calculate_checksum(recv_buff)) { +#ifdef DEBUG_VIA_USB + dump_buff("dropping partial rx: ", recv_buff, recv_len); +#endif recv_len = 0; } if (recv_len > 12) { +#ifdef DEBUG_VIA_USB + dump_buff("decoding successfull rx: ", recv_buff, recv_len); +#endif decode_packet(recv_buff[2], &recv_buff[4]); recv_len = 0; } diff --git a/Software/src/battery/DALY-BMS.h b/Software/src/battery/DALY-BMS.h index 7d00490a3..2ceee5de1 100644 --- a/Software/src/battery/DALY-BMS.h +++ b/Software/src/battery/DALY-BMS.h @@ -3,16 +3,17 @@ /* Tweak these according to your battery build */ #define CELL_COUNT 14 -#define DESIGN_PACK_VOLTAGE_DB 528 //528 = 52.8V -#define MAX_PACK_VOLTAGE_DV 588 //588 = 58.8V -#define MIN_PACK_VOLTAGE_DV 518 //518 = 51.8V -#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value -#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value -#define MAX_DISCHARGE_POWER_ALLOWED_W 1800 -#define MAX_CHARGE_POWER_ALLOWED_W 1800 +#define PACK_CAPACITY_AH 100 //100 = 100Ah +#define MAX_PACK_VOLTAGE_DV 588 //588 = 58.8V +#define MIN_PACK_VOLTAGE_DV 518 //518 = 51.8V +#define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value +#define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value +#define MAX_CHARGE_AMPS 32 +#define MAX_DISCHARGE_AMPS 32 /* Do not modify any rows below*/ #define BATTERY_SELECTED #define RS485_BATTERY_SELECTED +#define RS485_BAUDRATE 9600 #endif diff --git a/Software/src/communication/rs485/comm_rs485.cpp b/Software/src/communication/rs485/comm_rs485.cpp index 57d5bd8c9..4d5ba5086 100644 --- a/Software/src/communication/rs485/comm_rs485.cpp +++ b/Software/src/communication/rs485/comm_rs485.cpp @@ -30,7 +30,7 @@ void init_rs485() { digitalWrite(PIN_5V_EN, HIGH); #endif // PIN_5V_EN #if defined(RS485_INVERTER_SELECTED) || defined(RS485_BATTERY_SELECTED) - Serial2.begin(57600, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN); + Serial2.begin(RS485_BAUDRATE, SERIAL_8N1, RS485_RX_PIN, RS485_TX_PIN); #endif // RS485_INVERTER_SELECTED || RS485_BATTERY_SELECTED #ifdef MODBUS_INVERTER_SELECTED #ifdef BYD_MODBUS diff --git a/Software/src/inverter/KOSTAL-RS485.h b/Software/src/inverter/KOSTAL-RS485.h index 6f638ac6a..0d3c3f093 100644 --- a/Software/src/inverter/KOSTAL-RS485.h +++ b/Software/src/inverter/KOSTAL-RS485.h @@ -4,6 +4,7 @@ #include "../include.h" #define RS485_INVERTER_SELECTED +#define RS485_BAUDRATE 57600 //#define DEBUG_KOSTAL_RS485_DATA // Enable this line to get TX / RX printed out via logging #if defined(DEBUG_KOSTAL_RS485_DATA) && !defined(DEBUG_LOG) From dfa7d175a1883296ac93b0263941f61c68d26b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20L=C3=B6w?= Date: Fri, 7 Feb 2025 19:46:41 +0100 Subject: [PATCH 4/8] :bug: fix value scaling errors in pylon-lv-can inverter protocol --- Software/src/inverter/PYLON-LV-CAN.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Software/src/inverter/PYLON-LV-CAN.cpp b/Software/src/inverter/PYLON-LV-CAN.cpp index ddc2ce5a3..736fa349e 100644 --- a/Software/src/inverter/PYLON-LV-CAN.cpp +++ b/Software/src/inverter/PYLON-LV-CAN.cpp @@ -42,9 +42,12 @@ CAN_frame PYLON_35E = {.FD = false, void update_values_can_inverter() { // This function maps all the values fetched from battery CAN to the correct CAN messages - // TODO: officially this value is "battery charge voltage". Do we need to add something here to the actual voltage? - PYLON_351.data.u8[0] = datalayer.battery.status.voltage_dV & 0xff; - PYLON_351.data.u8[1] = datalayer.battery.status.voltage_dV >> 8; + // XXX: this value is "battery charge voltage". We add 1V here to the current voltage to achieve charging + int16_t charge_voltage_dV = datalayer.battery.status.voltage_dV + 1; + if (charge_voltage_dV > datalayer.battery.info.max_design_voltage_dV) + charge_voltage_dV = datalayer.battery.info.max_design_voltage_dV; + PYLON_351.data.u8[0] = charge_voltage_dV & 0xff; + PYLON_351.data.u8[1] = charge_voltage_dV >> 8; PYLON_351.data.u8[2] = datalayer.battery.status.max_charge_current_dA & 0xff; PYLON_351.data.u8[3] = datalayer.battery.status.max_charge_current_dA >> 8; PYLON_351.data.u8[4] = datalayer.battery.status.max_discharge_current_dA & 0xff; @@ -55,12 +58,16 @@ void update_values_can_inverter() { PYLON_355.data.u8[2] = (datalayer.battery.status.soh_pptt / 10) & 0xff; PYLON_355.data.u8[3] = (datalayer.battery.status.soh_pptt / 10) >> 8; - PYLON_356.data.u8[0] = datalayer.battery.status.voltage_dV & 0xff; - PYLON_356.data.u8[1] = datalayer.battery.status.voltage_dV >> 8; + int16_t voltage_cV = datalayer.battery.status.voltage_dV * 10; + int16_t temperature = datalayer.battery.status.temperature_min_dC; + if (datalayer.battery.status.temperature_max_dC > 20) + temperature = datalayer.battery.status.temperature_max_dC; + PYLON_356.data.u8[0] = voltage_cV & 0xff; + PYLON_356.data.u8[1] = voltage_cV >> 8; PYLON_356.data.u8[2] = datalayer.battery.status.current_dA & 0xff; PYLON_356.data.u8[3] = datalayer.battery.status.current_dA >> 8; - PYLON_356.data.u8[4] = datalayer.battery.status.temperature_max_dC & 0xff; - PYLON_356.data.u8[5] = datalayer.battery.status.temperature_max_dC >> 8; + PYLON_356.data.u8[4] = temperature & 0xff; + PYLON_356.data.u8[5] = temperature >> 8; // initialize all errors and warnings to 0 PYLON_359.data.u8[0] = 0x00; From 16266fe66cd676fbe4c31619dc47f72415dd4e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20L=C3=B6w?= Date: Mon, 10 Feb 2025 10:51:31 +0100 Subject: [PATCH 5/8] :sparkles: pylon LV: send battery-emulator BMS faults as "BMS internal" errors --- Software/src/inverter/PYLON-LV-CAN.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Software/src/inverter/PYLON-LV-CAN.cpp b/Software/src/inverter/PYLON-LV-CAN.cpp index 736fa349e..f6271341a 100644 --- a/Software/src/inverter/PYLON-LV-CAN.cpp +++ b/Software/src/inverter/PYLON-LV-CAN.cpp @@ -87,7 +87,8 @@ void update_values_can_inverter() { PYLON_359.data.u8[0] |= 0x0C; if (datalayer.battery.status.voltage_dV * 100 <= datalayer.battery.info.min_cell_voltage_mV) PYLON_359.data.u8[0] |= 0x04; - // we never set PYLON_359.data.u8[1] |= 0x80 called "BMS internal" + if (datalayer.battery.status.bms_status == FAULT) + PYLON_359.data.u8[1] |= 0x80; if (datalayer.battery.status.current_dA <= -1 * datalayer.battery.status.max_charge_current_dA) PYLON_359.data.u8[1] |= 0x01; From ba930eb2fb93339c8eff3f04354518cf1b7f2df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20L=C3=B6w?= Date: Mon, 10 Feb 2025 10:52:47 +0100 Subject: [PATCH 6/8] :sparkles: :lock: daly bms: add more sanity and safety checks --- Software/src/battery/DALY-BMS.cpp | 25 +++++++++++++++++++++++-- Software/src/battery/DALY-BMS.h | 1 + 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/Software/src/battery/DALY-BMS.cpp b/Software/src/battery/DALY-BMS.cpp index 240e94086..e62bc4411 100644 --- a/Software/src/battery/DALY-BMS.cpp +++ b/Software/src/battery/DALY-BMS.cpp @@ -19,6 +19,7 @@ static uint16_t cellvoltages_mV[48] = {0}; static uint16_t cellvoltage_min_mV = 0; static uint16_t cellvoltage_max_mV = 0; static uint16_t SOC = 0; +static bool has_fault = false; void update_values_battery() { datalayer.battery.status.real_soc = SOC; @@ -29,12 +30,25 @@ void update_values_battery() { datalayer.battery.status.max_charge_power_W = (MAX_CHARGE_AMPS * voltage_dV) / 10; datalayer.battery.status.max_discharge_power_W = (MAX_DISCHARGE_AMPS * voltage_dV) / 10; + uint32_t adaptive_power_limit = 999999; + if (SOC < 2000) + adaptive_power_limit = ((uint32_t)SOC * POWER_PER_PERCENT) / 100; + else if (SOC > 8000) + adaptive_power_limit = ((10000 - (uint32_t)SOC) * POWER_PER_PERCENT) / 100; + + if (adaptive_power_limit < datalayer.battery.status.max_charge_power_W) + datalayer.battery.status.max_charge_power_W = adaptive_power_limit; + if (adaptive_power_limit < datalayer.battery.status.max_discharge_power_W) + datalayer.battery.status.max_discharge_power_W = adaptive_power_limit; + memcpy(datalayer.battery.status.cell_voltages_mV, cellvoltages_mV, sizeof(cellvoltages_mV)); datalayer.battery.status.cell_min_voltage_mV = cellvoltage_min_mV; datalayer.battery.status.cell_max_voltage_mV = cellvoltage_max_mV; datalayer.battery.status.temperature_min_dC = temperature_min_dC; datalayer.battery.status.temperature_max_dC = temperature_max_dC; + + datalayer.battery.status.real_bms_status = has_fault ? BMS_FAULT : BMS_ACTIVE; } void setup_battery(void) { // Performs one time setup at startup @@ -119,6 +133,13 @@ void decode_packet(uint8_t command, uint8_t data[8]) { case 0x97: break; case 0x98: + // for now we do not handle individual faults. All of them are 0 when ok, and 1 when a fault occurs + has_fault = false; + for (int i = 0; i < 8; i++) { + if (data[i] != 0x00) { + has_fault = true; + } + } break; } } @@ -127,7 +148,7 @@ void transmit_rs485() { static uint32_t lastSend = 0; static uint8_t nextCommand = 0x90; - if (millis() - lastSend > 500) { + if (millis() - lastSend > 100) { uint8_t tx_buff[13] = {0}; tx_buff[0] = 0xA5; tx_buff[1] = 0x40; @@ -143,7 +164,7 @@ void transmit_rs485() { lastSend = millis(); nextCommand++; - if (nextCommand > 0x95) + if (nextCommand > 0x98) nextCommand = 0x90; } } diff --git a/Software/src/battery/DALY-BMS.h b/Software/src/battery/DALY-BMS.h index 2ceee5de1..162e5e25b 100644 --- a/Software/src/battery/DALY-BMS.h +++ b/Software/src/battery/DALY-BMS.h @@ -10,6 +10,7 @@ #define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value #define MAX_CHARGE_AMPS 32 #define MAX_DISCHARGE_AMPS 32 +#define POWER_PER_PERCENT 50 // below 20% and above 80% limit power to 50W * SOC (i.e. 150W at 3%, 500W at 10%, ...) /* Do not modify any rows below*/ #define BATTERY_SELECTED From 9712200e7a19a2894dc1ade1b4539861bb5e7cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20L=C3=B6w?= Date: Mon, 10 Feb 2025 18:51:48 +0100 Subject: [PATCH 7/8] :art: pylon lv: improve voltage border and warning handling according to review --- Software/src/inverter/PYLON-LV-CAN.cpp | 39 ++++++++++++++++++-------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/Software/src/inverter/PYLON-LV-CAN.cpp b/Software/src/inverter/PYLON-LV-CAN.cpp index f6271341a..463921e8d 100644 --- a/Software/src/inverter/PYLON-LV-CAN.cpp +++ b/Software/src/inverter/PYLON-LV-CAN.cpp @@ -39,11 +39,21 @@ CAN_frame PYLON_35E = {.FD = false, MANUFACTURER_NAME[7], }}; +// when e.g. the min temperature is 0, max is 100 and the warning percent is 80% +// a warning should be generated at 20 (i.e. at 20% of the value range) +// this function calculates this 20% point for a given min/max +int16_t warning_threshold_of_min(int16_t min_val, int16_t max_val) { + int16_t diff = max_val - min_val; + return min_val + (diff * (100 - WARNINGS_PERCENT)) / 100; +} + void update_values_can_inverter() { // This function maps all the values fetched from battery CAN to the correct CAN messages - // XXX: this value is "battery charge voltage". We add 1V here to the current voltage to achieve charging - int16_t charge_voltage_dV = datalayer.battery.status.voltage_dV + 1; + // Set "battery charge voltage" to volts + 1 or user supplied value + uint16_t charge_voltage_dV = datalayer.battery.status.voltage_dV + 1; + if (datalayer.battery.settings.user_set_voltage_limits_active) + charge_voltage_dV = datalayer.battery.settings.max_user_set_charge_voltage_dV; if (charge_voltage_dV > datalayer.battery.info.max_design_voltage_dV) charge_voltage_dV = datalayer.battery.info.max_design_voltage_dV; PYLON_351.data.u8[0] = charge_voltage_dV & 0xff; @@ -59,9 +69,7 @@ void update_values_can_inverter() { PYLON_355.data.u8[3] = (datalayer.battery.status.soh_pptt / 10) >> 8; int16_t voltage_cV = datalayer.battery.status.voltage_dV * 10; - int16_t temperature = datalayer.battery.status.temperature_min_dC; - if (datalayer.battery.status.temperature_max_dC > 20) - temperature = datalayer.battery.status.temperature_max_dC; + int16_t temperature = (datalayer.battery.status.temperature_min_dC + datalayer.battery.status.temperature_max_dC) / 2; PYLON_356.data.u8[0] = voltage_cV & 0xff; PYLON_356.data.u8[1] = voltage_cV >> 8; PYLON_356.data.u8[2] = datalayer.battery.status.current_dA & 0xff; @@ -85,7 +93,7 @@ void update_values_can_inverter() { PYLON_359.data.u8[0] |= 0x10; if (datalayer.battery.status.temperature_max_dC >= BATTERY_MAXTEMPERATURE) PYLON_359.data.u8[0] |= 0x0C; - if (datalayer.battery.status.voltage_dV * 100 <= datalayer.battery.info.min_cell_voltage_mV) + if (datalayer.battery.status.voltage_dV <= datalayer.battery.info.min_design_voltage_dV) PYLON_359.data.u8[0] |= 0x04; if (datalayer.battery.status.bms_status == FAULT) PYLON_359.data.u8[1] |= 0x80; @@ -95,11 +103,13 @@ void update_values_can_inverter() { // WARNINGS (using same rules as errors but reporting earlier) if (datalayer.battery.status.current_dA >= datalayer.battery.status.max_discharge_current_dA * WARNINGS_PERCENT / 100) PYLON_359.data.u8[2] |= 0x80; - if (datalayer.battery.status.temperature_min_dC <= BATTERY_MINTEMPERATURE * WARNINGS_PERCENT / 100) + if (datalayer.battery.status.temperature_min_dC <= + warning_threshold_of_min(BATTERY_MINTEMPERATURE, BATTERY_MAXTEMPERATURE)) PYLON_359.data.u8[2] |= 0x10; if (datalayer.battery.status.temperature_max_dC >= BATTERY_MAXTEMPERATURE * WARNINGS_PERCENT / 100) PYLON_359.data.u8[2] |= 0x0C; - if (datalayer.battery.status.voltage_dV * 100 <= datalayer.battery.info.min_cell_voltage_mV + 100) + if (datalayer.battery.status.voltage_dV <= warning_threshold_of_min(datalayer.battery.info.min_design_voltage_dV, + datalayer.battery.info.max_design_voltage_dV)) PYLON_359.data.u8[2] |= 0x04; // we never set PYLON_359.data.u8[3] |= 0x80 called "BMS internal" if (datalayer.battery.status.current_dA <= @@ -107,10 +117,17 @@ void update_values_can_inverter() { PYLON_359.data.u8[3] |= 0x01; PYLON_35C.data.u8[0] = 0xC0; // enable charging and discharging - PYLON_35C.data.u8[1] = 0x00; - if (datalayer.battery.status.real_soc <= datalayer.battery.settings.min_percentage) + if (datalayer.battery.status.bms_status == FAULT) + PYLON_35C.data.u8[1] = 0x00; // disable all + else if (datalayer.battery.settings.user_set_voltage_limits_active && + datalayer.battery.status.voltage_dV > datalayer.battery.settings.max_user_set_charge_voltage_dV) + PYLON_35C.data.u8[1] = 0x40; // only allow discharging + else if (datalayer.battery.settings.user_set_voltage_limits_active && + datalayer.battery.status.voltage_dV < datalayer.battery.settings.max_user_set_discharge_voltage_dV) + PYLON_35C.data.u8[1] = 0xA0; // enable charing, set charge immediately + else if (datalayer.battery.status.real_soc <= datalayer.battery.settings.min_percentage) PYLON_35C.data.u8[0] = 0xA0; // enable charing, set charge immediately - if (datalayer.battery.status.real_soc >= datalayer.battery.settings.max_percentage) + else if (datalayer.battery.status.real_soc >= datalayer.battery.settings.max_percentage) PYLON_35C.data.u8[0] = 0x40; // enable discharging only // PYLON_35E is pre-filled with the manufacturer name From 25e30d07583949ee3aa2466ca5d2ce32f96dda73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20L=C3=B6w?= Date: Mon, 10 Feb 2025 18:52:31 +0100 Subject: [PATCH 8/8] :art: daly bms: use user settings pre-defined values instead of redefining them ourselves --- Software/src/battery/DALY-BMS.cpp | 6 +++--- Software/src/battery/DALY-BMS.h | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Software/src/battery/DALY-BMS.cpp b/Software/src/battery/DALY-BMS.cpp index e62bc4411..974abd5a0 100644 --- a/Software/src/battery/DALY-BMS.cpp +++ b/Software/src/battery/DALY-BMS.cpp @@ -27,8 +27,8 @@ void update_values_battery() { datalayer.battery.status.current_dA = current_dA; //value is *10 (150 = 15.0) datalayer.battery.status.remaining_capacity_Wh = (remaining_capacity_mAh * (uint32_t)voltage_dV) / 10000; - datalayer.battery.status.max_charge_power_W = (MAX_CHARGE_AMPS * voltage_dV) / 10; - datalayer.battery.status.max_discharge_power_W = (MAX_DISCHARGE_AMPS * voltage_dV) / 10; + datalayer.battery.status.max_charge_power_W = (BATTERY_MAX_CHARGE_AMP * voltage_dV) / 100; + datalayer.battery.status.max_discharge_power_W = (BATTERY_MAX_DISCHARGE_AMP * voltage_dV) / 100; uint32_t adaptive_power_limit = 999999; if (SOC < 2000) @@ -59,7 +59,7 @@ void setup_battery(void) { // Performs one time setup at startup datalayer.battery.info.min_design_voltage_dV = MIN_PACK_VOLTAGE_DV; datalayer.battery.info.max_cell_voltage_mV = MAX_CELL_VOLTAGE_MV; datalayer.battery.info.min_cell_voltage_mV = MIN_CELL_VOLTAGE_MV; - datalayer.battery.info.total_capacity_Wh = PACK_CAPACITY_AH; + datalayer.battery.info.total_capacity_Wh = BATTERY_WH_MAX; } uint8_t calculate_checksum(uint8_t buff[12]) { diff --git a/Software/src/battery/DALY-BMS.h b/Software/src/battery/DALY-BMS.h index 162e5e25b..dfc3df02d 100644 --- a/Software/src/battery/DALY-BMS.h +++ b/Software/src/battery/DALY-BMS.h @@ -3,13 +3,10 @@ /* Tweak these according to your battery build */ #define CELL_COUNT 14 -#define PACK_CAPACITY_AH 100 //100 = 100Ah #define MAX_PACK_VOLTAGE_DV 588 //588 = 58.8V #define MIN_PACK_VOLTAGE_DV 518 //518 = 51.8V #define MAX_CELL_VOLTAGE_MV 4250 //Battery is put into emergency stop if one cell goes over this value #define MIN_CELL_VOLTAGE_MV 2700 //Battery is put into emergency stop if one cell goes below this value -#define MAX_CHARGE_AMPS 32 -#define MAX_DISCHARGE_AMPS 32 #define POWER_PER_PERCENT 50 // below 20% and above 80% limit power to 50W * SOC (i.e. 150W at 3%, 500W at 10%, ...) /* Do not modify any rows below*/