diff --git a/.github/workflows/compile-all-combinations-part1-batteries-A-to-M.yml b/.github/workflows/compile-all-combinations-part1-batteries-A-to-M.yml index 47d9a4fcd..2926e091a 100644 --- a/.github/workflows/compile-all-combinations-part1-batteries-A-to-M.yml +++ b/.github/workflows/compile-all-combinations-part1-batteries-A-to-M.yml @@ -73,6 +73,7 @@ jobs: - BYD_KOSTAL_RS485 - BYD_MODBUS - FOXESS_CAN + - GROWATT_LV_CAN - PYLON_CAN - PYLON_LV_CAN - SCHNEIDER_CAN diff --git a/.github/workflows/compile-all-combinations-part2-batteries-N-to-Z.yml b/.github/workflows/compile-all-combinations-part2-batteries-N-to-Z.yml index c7c5ca5a4..c1233dd04 100644 --- a/.github/workflows/compile-all-combinations-part2-batteries-N-to-Z.yml +++ b/.github/workflows/compile-all-combinations-part2-batteries-N-to-Z.yml @@ -75,6 +75,7 @@ jobs: - BYD_KOSTAL_RS485 - BYD_MODBUS - FOXESS_CAN + - GROWATT_LV_CAN - PYLON_CAN - PYLON_LV_CAN - SCHNEIDER_CAN diff --git a/.github/workflows/compile-all-inverters.yml b/.github/workflows/compile-all-inverters.yml index f1b0fcf54..24b9f8469 100644 --- a/.github/workflows/compile-all-inverters.yml +++ b/.github/workflows/compile-all-inverters.yml @@ -59,6 +59,7 @@ jobs: - BYD_KOSTAL_RS485 - BYD_MODBUS - FOXESS_CAN + - GROWATT_LV_CAN - PYLON_CAN - PYLON_LV_CAN - SCHNEIDER_CAN diff --git a/Software/USER_SETTINGS.h b/Software/USER_SETTINGS.h index 1efe7980a..0e5843344 100644 --- a/Software/USER_SETTINGS.h +++ b/Software/USER_SETTINGS.h @@ -45,6 +45,7 @@ //#define BYD_KOSTAL_RS485 //Enable this line to emulate a "BYD 11kWh HVM battery" over Kostal RS485 //#define BYD_MODBUS //Enable this line to emulate a "BYD 11kWh HVM battery" over Modbus RTU //#define FOXESS_CAN //Enable this line to emulate a "HV2600/ECS4100 battery" over CAN bus +//#define GROWATT_LV_CAN //Enable this line to emulate a "48V Growatt Low Voltage battery" over CAN bus //#define PYLON_LV_CAN //Enable this line to emulate a "48V Pylontech battery" over CAN bus //#define PYLON_CAN //Enable this line to emulate a "High Voltage Pylontech battery" over CAN bus //#define SCHNEIDER_CAN //Enable this line to emulate a "Schneider Version 2: SE BMS" over CAN bus diff --git a/Software/src/inverter/GROWATT-LV-CAN.cpp b/Software/src/inverter/GROWATT-LV-CAN.cpp new file mode 100644 index 000000000..01f62619c --- /dev/null +++ b/Software/src/inverter/GROWATT-LV-CAN.cpp @@ -0,0 +1,278 @@ +#include "../include.h" +#ifdef GROWATT_LV_CAN +#include "../datalayer/datalayer.h" +#include "GROWATT-LV-CAN.h" + +/* Growatt BMS CAN-Bus-protocol Low Voltage Rev_04 +CAN 2.0A +500kBit/sec +Big-endian + +The inverter replies data every second (standard frame/decimal)0x301:*/ + +/* Do not change code below unless you are sure what you are doing */ + +//Actual content messages +CAN_frame GROWATT_311 = {.FD = false, //Voltage and charge limits and status + .ext_ID = false, + .DLC = 8, + .ID = 0x311, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame GROWATT_312 = {.FD = false, //status bits , pack number, total cell number + .ext_ID = false, + .DLC = 8, + .ID = 0x312, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame GROWATT_313 = {.FD = false, //voltage, current, temp, soc, soh + .ext_ID = false, + .DLC = 8, + .ID = 0x313, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame GROWATT_314 = {.FD = false, //capacity, delta V, cycle count + .ext_ID = false, + .DLC = 8, + .ID = 0x314, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame GROWATT_319 = {.FD = false, //max/min cell voltage, num of cell max/min, protect pack ID + .ext_ID = false, + .DLC = 8, + .ID = 0x319, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame GROWATT_320 = {.FD = false, //manufacturer name, hw ver, sw ver, date and time + .ext_ID = false, + .DLC = 8, + .ID = 0x320, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame GROWATT_321 = {.FD = false, //Update status, ID + .ext_ID = false, + .DLC = 8, + .ID = 0x321, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + +//Cellvoltages +CAN_frame GROWATT_315 = {.FD = false, //Cells 1-4 + .ext_ID = false, + .DLC = 8, + .ID = 0x315, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame GROWATT_316 = {.FD = false, //Cells 5-8 + .ext_ID = false, + .DLC = 8, + .ID = 0x316, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame GROWATT_317 = {.FD = false, //Cells 9-12 + .ext_ID = false, + .DLC = 8, + .ID = 0x317, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; +CAN_frame GROWATT_318 = {.FD = false, //Cells 13-16 + .ext_ID = false, + .DLC = 8, + .ID = 0x318, + .data = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}}; + +#define VOLTAGE_OFFSET_DV 40 //Offset in deciVolt from max charge voltage and min discharge voltage +#define MAX_VOLTAGE_DV 630 +#define MIN_VOLTAGE_DV 410 + +static uint16_t cell_delta_mV = 0; +static uint16_t ampere_hours_remaining = 0; +static uint16_t ampere_hours_full = 0; + +void update_values_can_inverter() { //This function maps all the values fetched from battery CAN to the correct CAN messages + + cell_delta_mV = datalayer.battery.status.cell_max_voltage_mV - datalayer.battery.status.cell_min_voltage_mV; + + if (datalayer.battery.status.voltage_dV > 10) { // Only update value when we have voltage available to avoid div0 + ampere_hours_remaining = + ((datalayer.battery.status.reported_remaining_capacity_Wh / datalayer.battery.status.voltage_dV) * + 100); //(WH[10000] * V+1[3600])*100 = 270 (27.0Ah) + ampere_hours_full = ((datalayer.battery.info.total_capacity_Wh / datalayer.battery.status.voltage_dV) * + 100); //(WH[10000] * V+1[3600])*100 = 270 (27.0Ah) + } + //Map values to CAN messages + + //Battery charge voltage (eg 400.0V = 4000 , 16bits long) (MIN 41V, MAX 63V, default 54V) + GROWATT_311.data.u8[0] = ((datalayer.battery.info.max_design_voltage_dV - VOLTAGE_OFFSET_DV) >> 8); + GROWATT_311.data.u8[1] = ((datalayer.battery.info.max_design_voltage_dV - VOLTAGE_OFFSET_DV) & 0x00FF); + //Charge limited current, 125 =12.5A (0.1, A) + GROWATT_311.data.u8[2] = (datalayer.battery.status.max_charge_current_dA >> 8); + GROWATT_311.data.u8[3] = (datalayer.battery.status.max_charge_current_dA & 0x00FF); + //Discharge limited current, 500 = 50A, (0.1, A) + GROWATT_311.data.u8[4] = (datalayer.battery.status.max_discharge_current_dA >> 8); + GROWATT_311.data.u8[5] = (datalayer.battery.status.max_discharge_current_dA & 0x00FF); + //Status bits (see documentation for all bits, most important are bit0-1 (Status), and bit 10-11 (SP status)) + if (datalayer.battery.status.active_power_W < -1) { // Discharging + GROWATT_311.data.u8[6] = 0x0C; //0b11 discharging on bit10-11 + GROWATT_311.data.u8[7] = 0x03; //0b11 discharging on bit0-1 + } else if (datalayer.battery.status.active_power_W > 1) { // Charging + GROWATT_311.data.u8[6] = 0x08; //0b10 charging on bit10-11 + GROWATT_311.data.u8[7] = 0x02; //0b10 charging on bit0-1 + } else { //Idle + GROWATT_311.data.u8[6] = 0x04; //0b01 charging on bit10-11 + GROWATT_311.data.u8[7] = 0x01; //0b01 charging on bit0-1 + } + + //Fault status bits. TODO, map these according to docmentation. + //GROWATT_312.data.u8[0] = + //GROWATT_312.data.u8[1] = + //GROWATT_312.data.u8[2] = + //GROWATT_312.data.u8[3] = + GROWATT_312.data.u8[4] = 0x01; // Pack number + GROWATT_312.data.u8[5] = 0xAA; // Manufacturer code + GROWATT_312.data.u8[6] = 0xBB; // Manufacturer code + GROWATT_312.data.u8[7] = datalayer.battery.info.number_of_cells; // Total cell number (1-254) + + //Voltage of single module or Average module voltage of system (0.01V) + GROWATT_313.data.u8[0] = ((datalayer.battery.status.voltage_dV * 10) >> 8); + GROWATT_313.data.u8[1] = ((datalayer.battery.status.voltage_dV * 10) & 0x00FF); + //Module or system total current (0.1A Sint16) + GROWATT_313.data.u8[2] = (datalayer.battery.status.current_dA >> 8); + GROWATT_313.data.u8[3] = (datalayer.battery.status.current_dA & 0x00FF); + //Cell max temperature (0.1C) + GROWATT_313.data.u8[4] = (datalayer.battery.status.temperature_max_dC >> 8); + GROWATT_313.data.u8[5] = (datalayer.battery.status.temperature_max_dC & 0x00FF); + //SOC of single module or average value of system (%) + GROWATT_313.data.u8[6] = (datalayer.battery.status.reported_soc / 100); + //SOH (%) (Bit 0~ Bit6 SOH Counters) Bit7 SOH flag (unsure what this is) + GROWATT_313.data.u8[7] = (datalayer.battery.status.soh_pptt / 100); + + //Remaining capacity (10 mAh) + GROWATT_314.data.u8[0] = ((ampere_hours_remaining * 100) >> 8); + GROWATT_314.data.u8[1] = ((ampere_hours_remaining * 100) & 0x00FF); + //Fully charged capacity (10 mAh) + GROWATT_314.data.u8[2] = ((ampere_hours_full * 100) >> 8); + GROWATT_314.data.u8[3] = ((ampere_hours_full * 100) & 0x00FF); + //Delta V (mV) + GROWATT_314.data.u8[4] = (cell_delta_mV >> 8); + GROWATT_314.data.u8[5] = (cell_delta_mV & 0x00FF); + //Cycle count (h) + GROWATT_314.data.u8[6] = 0; + GROWATT_314.data.u8[7] = 0; + + //Request charge/discharge + if (datalayer.battery.status.bms_status == ACTIVE) { + GROWATT_319.data.u8[0] = + 0xC0; //Bit7 charge enabled, Bit 6 discharge enabled (bit5 req force charge, bit 4 req force charge 2) + } else { + GROWATT_319.data.u8[0] = 0x00; + } + //TODO: if battery falls below SOC 5% during long idle time, we should set bit 5 + + //Maximum cell voltage (mV) + GROWATT_319.data.u8[1] = (datalayer.battery.status.cell_max_voltage_mV >> 8); + GROWATT_319.data.u8[2] = (datalayer.battery.status.cell_max_voltage_mV & 0x00FF); + // Min cell voltage (mV) + GROWATT_319.data.u8[3] = (datalayer.battery.status.cell_min_voltage_mV >> 8); + GROWATT_319.data.u8[4] = (datalayer.battery.status.cell_min_voltage_mV & 0x00FF); + //Maximum cell voltage number + GROWATT_319.data.u8[5] = 1; //Fake + // Min cell voltage number + GROWATT_319.data.u8[6] = 2; //Fake + //Protect pack ID + GROWATT_319.data.u8[7] = 0; //? + + // Manufacturer name (ASCII) Battery manufacturer abbreviation in capital letters + GROWATT_320.data.u8[0] = 0x42; //B + GROWATT_320.data.u8[1] = 0x45; //E + // Hardware revision (1-9) + GROWATT_320.data.u8[2] = 0x01; + // Software version (1-9) + GROWATT_320.data.u8[3] = 0x01; + //Date and Time + //Bit 0~5 Second0~59 + //Bit 6~11 Minute0~59 + //Bit 12~16 Hour0~23 + //Bit 17~21Day1~31 + //Bit 22~25 Month 1-12 + //Bit 26~31 Year (2000-2063) + GROWATT_320.data.u8[4] = 0; //TODO + GROWATT_320.data.u8[5] = 0; + GROWATT_320.data.u8[6] = 0; + GROWATT_320.data.u8[7] = 0; + + //Message 0x321 is update status. All blank is OK + + //Cellvoltage #1 + GROWATT_315.data.u8[0] = (datalayer.battery.status.cell_voltages_mV[0] >> 8); + GROWATT_315.data.u8[1] = (datalayer.battery.status.cell_voltages_mV[0] & 0x00FF); + //Cellvoltage #2 + GROWATT_315.data.u8[2] = (datalayer.battery.status.cell_voltages_mV[1] >> 8); + GROWATT_315.data.u8[3] = (datalayer.battery.status.cell_voltages_mV[1] & 0x00FF); + //Cellvoltage #3 + GROWATT_315.data.u8[4] = (datalayer.battery.status.cell_voltages_mV[2] >> 8); + GROWATT_315.data.u8[5] = (datalayer.battery.status.cell_voltages_mV[2] & 0x00FF); + //Cellvoltage #4 + GROWATT_315.data.u8[6] = (datalayer.battery.status.cell_voltages_mV[3] >> 8); + GROWATT_315.data.u8[7] = (datalayer.battery.status.cell_voltages_mV[3] & 0x00FF); + + //Cellvoltage #5 + GROWATT_316.data.u8[0] = (datalayer.battery.status.cell_voltages_mV[4] >> 8); + GROWATT_316.data.u8[1] = (datalayer.battery.status.cell_voltages_mV[4] & 0x00FF); + //Cellvoltage #6 + GROWATT_316.data.u8[2] = (datalayer.battery.status.cell_voltages_mV[5] >> 8); + GROWATT_316.data.u8[3] = (datalayer.battery.status.cell_voltages_mV[5] & 0x00FF); + //Cellvoltage #7 + GROWATT_316.data.u8[4] = (datalayer.battery.status.cell_voltages_mV[6] >> 8); + GROWATT_316.data.u8[5] = (datalayer.battery.status.cell_voltages_mV[6] & 0x00FF); + //Cellvoltage #8 + GROWATT_316.data.u8[6] = (datalayer.battery.status.cell_voltages_mV[7] >> 8); + GROWATT_316.data.u8[7] = (datalayer.battery.status.cell_voltages_mV[7] & 0x00FF); + + //Cellvoltage #9 + GROWATT_317.data.u8[0] = (datalayer.battery.status.cell_voltages_mV[8] >> 8); + GROWATT_317.data.u8[1] = (datalayer.battery.status.cell_voltages_mV[8] & 0x00FF); + //Cellvoltage #10 + GROWATT_317.data.u8[2] = (datalayer.battery.status.cell_voltages_mV[9] >> 8); + GROWATT_317.data.u8[3] = (datalayer.battery.status.cell_voltages_mV[9] & 0x00FF); + //Cellvoltage #11 + GROWATT_317.data.u8[4] = (datalayer.battery.status.cell_voltages_mV[10] >> 8); + GROWATT_317.data.u8[5] = (datalayer.battery.status.cell_voltages_mV[10] & 0x00FF); + //Cellvoltage #12 + GROWATT_317.data.u8[6] = (datalayer.battery.status.cell_voltages_mV[11] >> 8); + GROWATT_317.data.u8[7] = (datalayer.battery.status.cell_voltages_mV[11] & 0x00FF); + + //Cellvoltage #13 + GROWATT_318.data.u8[0] = (datalayer.battery.status.cell_voltages_mV[12] >> 8); + GROWATT_318.data.u8[1] = (datalayer.battery.status.cell_voltages_mV[12] & 0x00FF); + //Cellvoltage #14 + GROWATT_318.data.u8[2] = (datalayer.battery.status.cell_voltages_mV[13] >> 8); + GROWATT_318.data.u8[3] = (datalayer.battery.status.cell_voltages_mV[13] & 0x00FF); + //Cellvoltage #15 + GROWATT_318.data.u8[4] = (datalayer.battery.status.cell_voltages_mV[14] >> 8); + GROWATT_318.data.u8[5] = (datalayer.battery.status.cell_voltages_mV[14] & 0x00FF); + //Cellvoltage #16 + GROWATT_318.data.u8[6] = (datalayer.battery.status.cell_voltages_mV[15] >> 8); + GROWATT_318.data.u8[7] = (datalayer.battery.status.cell_voltages_mV[15] & 0x00FF); +} + +void map_can_frame_to_variable_inverter(CAN_frame rx_frame) { + switch (rx_frame.ID) { + case 0x301: + datalayer.system.status.CAN_inverter_still_alive = CAN_STILL_ALIVE; + transmit_can_frame(&GROWATT_311, can_config.inverter); + transmit_can_frame(&GROWATT_312, can_config.inverter); + transmit_can_frame(&GROWATT_313, can_config.inverter); + transmit_can_frame(&GROWATT_314, can_config.inverter); + transmit_can_frame(&GROWATT_315, can_config.inverter); + transmit_can_frame(&GROWATT_316, can_config.inverter); + transmit_can_frame(&GROWATT_317, can_config.inverter); + transmit_can_frame(&GROWATT_318, can_config.inverter); + transmit_can_frame(&GROWATT_319, can_config.inverter); + transmit_can_frame(&GROWATT_320, can_config.inverter); + transmit_can_frame(&GROWATT_321, can_config.inverter); + break; + default: + break; + } +} + +void transmit_can_inverter() { + // No periodic sending for this battery type. Data is sent when inverter requests it +} + +void setup_inverter(void) { // Performs one time setup at startup over CAN bus + strncpy(datalayer.system.info.inverter_protocol, "Growatt Low Voltage (48V) protocol via CAN", 63); + datalayer.system.info.inverter_protocol[63] = '\0'; +} +#endif diff --git a/Software/src/inverter/GROWATT-LV-CAN.h b/Software/src/inverter/GROWATT-LV-CAN.h new file mode 100644 index 000000000..fe6dce341 --- /dev/null +++ b/Software/src/inverter/GROWATT-LV-CAN.h @@ -0,0 +1,10 @@ +#ifndef GROWATT_LV_CAN_H +#define GROWATT_LV_CAN_H +#include "../include.h" + +#define CAN_INVERTER_SELECTED + +void transmit_can_frame(CAN_frame* tx_frame, int interface); +void setup_inverter(void); + +#endif diff --git a/Software/src/inverter/INVERTERS.h b/Software/src/inverter/INVERTERS.h index 8d379d7d0..30786dc4f 100644 --- a/Software/src/inverter/INVERTERS.h +++ b/Software/src/inverter/INVERTERS.h @@ -23,6 +23,10 @@ #include "FOXESS-CAN.h" #endif +#ifdef GROWATT_LV_CAN +#include "GROWATT-LV-CAN.h" +#endif + #ifdef PYLON_CAN #include "PYLON-CAN.h" #endif