From 011154993eeac95c6d1d2e8b59847a407c115033 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Tue, 17 Aug 2021 16:03:53 +0100 Subject: [PATCH 1/6] Change MODBUS registers to hold SOC values --- Code/diybmsCurrentShunt/MODBUS Registers.md | 104 +++++++------ Code/diybmsCurrentShunt/src/main.cpp | 161 ++++++++++++++------ 2 files changed, 176 insertions(+), 89 deletions(-) diff --git a/Code/diybmsCurrentShunt/MODBUS Registers.md b/Code/diybmsCurrentShunt/MODBUS Registers.md index 729d844..a032c58 100644 --- a/Code/diybmsCurrentShunt/MODBUS Registers.md +++ b/Code/diybmsCurrentShunt/MODBUS Registers.md @@ -27,60 +27,72 @@ All registers are read only, unless also specified in "Write Registers" later on |40017|shunt_resistance (4 byte double) |40018|shunt_resistance |40019|shunt_max_current (unsigned int16) -40020|shunt_millivolt (unsigned int16) -40021|INA_REGISTER::SHUNT_CAL (unsigned int16) -40022|Temperature limit (signed int16) -40023|Bus Overvoltage (overvoltage protection)(4 byte double) -40024|Bus Overvoltage (overvoltage protection) -40025|BusUnderVolt (4 byte double) -40026|BusUnderVolt -40027|Shunt Over Voltage Limit (current limit) (4 byte double) -40028|Shunt Over Voltage Limit (current limit) -40029|Shunt UNDER Voltage Limit (under current limit) (4 byte double) -40030|Shunt UNDER Voltage Limit (under current limit) -40031|Shunt Over POWER LIMIT (4 byte double) -40032|Shunt Over POWER LIMIT -40033|Shunt Temperature Coefficient (SHUNT_TEMPCO) (unsigned int16) -40034|INAXXX chip model number (should always be 0x0228) -40035|GITHUB version -40036|GITHUB version -40037|COMPILE_DATE_TIME_EPOCH -40038|COMPILE_DATE_TIME_EPOCH -40039|Watchdog timer trigger count (like error counter) -40040|DEBUG CONFIG -40041|DEBUG ADC_CONFIG -40042|DEBUG SHUNT_CAL -40043|DEBUG SHUNT_TEMPCO -40044|DEBUG DIAG_ALRT -40045|DEBUG SOVL -40046|DEBUG SUVL -40047|DEBUG BOVL -40049|DEBUG BUVL -40050|DEBUG TEMP_LIMIT -40051|DEBUG PWR_LIMIT -40052|DEBUG DIETEMP - +|40020|shunt_millivolt (unsigned int16) + +|40021|Battery Capacity (ah) (unsigned int16) +|40022|Fully charged voltage (4 byte double) +|40023|Fully charged voltage +|40024|Tail current (Amps) (4 byte double) +|40025|Tail current (Amps) +|40026|Charge efficiency factor % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 9561 = 95.61%) +|40027|State of charge % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 8012 = 80.12%, 100 = 1.00%) + +|40028|INA_REGISTER::SHUNT_CAL (unsigned int16) +|40029|Temperature limit (signed int16) +|40030|Bus Overvoltage (overvoltage protection)(4 byte double) +|40031|Bus Overvoltage (overvoltage protection) +|40032|BusUnderVolt (4 byte double) +|40033|BusUnderVolt +|40034|Shunt Over Voltage Limit (current limit) (4 byte double) +|40035|Shunt Over Voltage Limit (current limit) +|40036|Shunt UNDER Voltage Limit (under current limit) (4 byte double) +|40037|Shunt UNDER Voltage Limit (under current limit) +|40038|Shunt Over POWER LIMIT (4 byte double) +|40039|Shunt Over POWER LIMIT +|40040|Shunt Temperature Coefficient (SHUNT_TEMPCO) (unsigned int16) +|40041|INAXXX chip model number (should always be 0x0228) +|40042|GITHUB version +|40043|GITHUB version +|40044|COMPILE_DATE_TIME_EPOCH +|40045|COMPILE_DATE_TIME_EPOCH +|40046|Watchdog timer trigger count (like error counter) +|40047|DEBUG CONFIG +|40048|DEBUG ADC_CONFIG +|40049|DEBUG SHUNT_CAL +|40050|DEBUG SHUNT_TEMPCO +|40051|DEBUG DIAG_ALRT +|40052|DEBUG SOVL +|40053|DEBUG SUVL +|40054|DEBUG BOVL +|40055|DEBUG BUVL +|40056|DEBUG TEMP_LIMIT +|40057|DEBUG PWR_LIMIT +|40058|DEBUG DIETEMP # 0x16 Write Multiple Registers |Register|Description|Example| |--------|-----------|-------| -40005/40006|amphour_out|Set to zero to reset -40007/40008|amphour_in|Set to zero to reset -40010|Watchdog timer trigger count (like error counter) (unsigned int16)| -40019|shunt_max_current (unsigned int16) |e.g. 150 -40020|shunt_millivolt (unsigned int16) |e.g. 50 -40021|SHUNT_CAL (unsigned int16) ** -40022|Temperature limit (signed int16)|degrees C) -40023/40024|Bus Overvoltage (overvoltage protection)(4 byte double)|Volts -40025/40026|Bus Under Volt (4 byte double) |Volts -40027/40028|Shunt Over Voltage Limit (current limit) (4 byte double)|Current -40029/40030|Shunt UNDER Voltage Limit (under current limit) (4 byte double)|Current -40031/40032|Shunt Over POWER LIMIT (4 byte double)|Power +|40005/40006|amphour_out|Set to zero to reset +|40007/40008|amphour_in|Set to zero to reset +|40010|Watchdog timer trigger count (like error counter) (unsigned int16)| +|40019|shunt_max_current (unsigned int16) |e.g. 150 +|40020|shunt_millivolt (unsigned int16) |e.g. 50 +|40021/40022|Battery Capacity (ah) (unsigned int16) +|40023/40024|Fully charged voltage +|40025|Tail current (Amps) +|40026|Charge efficiency factor % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 9561 = 95.61%) +|40028|SHUNT_CAL (unsigned int16) ** +|40029|Temperature limit (signed int16)|degrees C) +|40030/40031|Bus Overvoltage (overvoltage protection)(4 byte double)|Volts +|40032/40033|Bus Under Volt (4 byte double) |Volts +|40034/40035|Shunt Over Voltage Limit (current limit) (4 byte double)|Current +|40036/40037|Shunt UNDER Voltage Limit (under current limit) (4 byte double)|Current +|40038/40039|Shunt Over POWER LIMIT (4 byte double)|Power ** For normal configuration, just set the shunt maximum current and millivolt registers. Shunt_Cal is calculated as needed based on those values. For fine tuning calibration, the SHUNT_CAL register can be written to/adjusted. Once a register is set, it is stored in EEPROM and used when shunt is powered up. -# Bit values for Register 40010 (status flags) +# Bit values for Register 40017 (status flags) Returns a BIT value (on/off) for the following configuration items: diff --git a/Code/diybmsCurrentShunt/src/main.cpp b/Code/diybmsCurrentShunt/src/main.cpp index 9a21f0e..b47fe38 100644 --- a/Code/diybmsCurrentShunt/src/main.cpp +++ b/Code/diybmsCurrentShunt/src/main.cpp @@ -505,11 +505,15 @@ bool SetRegister(uint16_t address, uint16_t value) { case 4: case 6: - case 22: - case 24: - case 26: - case 28: - case 30: + //|40022|Fully charged voltage (4 byte double) + case 21: + //|40024|Tail current (Amps) (4 byte double) + case 23: + case 29: + case 31: + case 33: + case 35: + case 37: { //Set word[0] in preperation for the next register to be written newvalue.word[0] = value; @@ -589,6 +593,7 @@ bool SetRegister(uint16_t address, uint16_t value) } case 19: { + //Register 40020 registers.shunt_millivolt = value; CalculateLSB(); break; @@ -596,12 +601,40 @@ bool SetRegister(uint16_t address, uint16_t value) case 20: { + //|40021|Battery Capacity (ah) (unsigned int16) + break; + } + case 22: + { + //|40023|Fully charged voltage + break; + } + + case 24: + { + //|40025|Tail current (Amps) + break; + } + case 25: + { + //|40026|Charge efficiency factor % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 9561 = 95.61%) + break; + } + case 26: + { + //|40027|State of charge % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 8012 = 80.12%, 100 = 1.00%) + break; + } + + case 27: + { + //Register 40028 //SHUNT_CAL register registers.R_SHUNT_CAL = value; break; } - case 21: + case 28: { //temperature limit //Case unsigned to int16 to cope with negative temperatures @@ -609,7 +642,7 @@ bool SetRegister(uint16_t address, uint16_t value) break; } - case 23: + case 30: { //Bus Overvoltage (overvoltage protection). //Unsigned representation, positive value only. Conversion factor: 3.125 mV/LSB. @@ -620,7 +653,7 @@ bool SetRegister(uint16_t address, uint16_t value) break; } - case 25: + case 32: { //Bus under voltage newvalue.word[1] = value; @@ -628,7 +661,7 @@ bool SetRegister(uint16_t address, uint16_t value) break; } - case 27: + case 34: { //Shunt Over Voltage Limit (current limit) newvalue.word[1] = value; @@ -637,7 +670,7 @@ bool SetRegister(uint16_t address, uint16_t value) break; } - case 29: + case 36: { //Shunt UNDER Voltage Limit (under current limit) newvalue.word[1] = value; @@ -645,7 +678,7 @@ bool SetRegister(uint16_t address, uint16_t value) break; } - case 31: + case 38: { //Shunt Over POWER LIMIT newvalue.word[1] = value; @@ -653,14 +686,14 @@ bool SetRegister(uint16_t address, uint16_t value) break; } - case 32: + case 39: { //Shunt Temperature Coefficient registers.R_SHUNT_TEMPCO = value; break; } - case 38: + case 45: { //Watchdog timer trigger count (like error counter) wdt_triggered_count = value; @@ -1189,20 +1222,62 @@ uint16_t ReadHoldingRegister(uint16_t address) } case 20: + { + //|40021|Battery Capacity (ah) (unsigned int16) + return 0; + break; + } + case 21: + { + //|40022|Fully charged voltage (4 byte double) + return 0; + break; + } + case 22: + { + //|40023|Fully charged voltage + return 0; + break; + } + case 23: + { + //|40024|Tail current (Amps) (4 byte double) + return 0; + break; + } + case 24: + { + //|40025|Tail current (Amps) + return 0; + break; + } + case 25: + { + //|40026|Charge efficiency factor % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 9561 = 95.61%) + return 0; + break; + } + case 26: + { + //|40027|State of charge % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 8012 = 80.12%, 100 = 1.00%) + return 0; + break; + } + case 27: { //SHUNT_CAL register return i2c_readword(INA_REGISTER::SHUNT_CAL); break; } - case 21: + case 28: { //temperature limit return (int16_t)TemperatureLimit(); break; } - case 22: + case 29: { //Bus Overvoltage (overvoltage protection). //Unsigned representation, positive value only. Conversion factor: 3.125 mV/LSB. @@ -1211,26 +1286,26 @@ uint16_t ReadHoldingRegister(uint16_t address) break; } - case 23: + case 30: { return BusOverVolt.word[1]; break; } - case 24: + case 31: { BusUnderVolt.dblvalue = (double)i2c_readword(INA_REGISTER::BUVL) * 0.003125F; return BusUnderVolt.word[0]; break; } - case 25: + case 32: { return BusUnderVolt.word[1]; break; } - case 26: + case 33: { //Shunt Over Voltage Limit (current limit) int16_t value = i2c_readword(INA_REGISTER::SOVL); @@ -1241,13 +1316,13 @@ uint16_t ReadHoldingRegister(uint16_t address) return ShuntOverCurrentLimit.word[0]; break; } - case 27: + case 34: { return ShuntOverCurrentLimit.word[1]; break; } - case 28: + case 35: { //Shunt UNDER Voltage Limit (under current limit) int16_t value = i2c_readword(INA_REGISTER::SUVL); @@ -1260,13 +1335,13 @@ uint16_t ReadHoldingRegister(uint16_t address) return ShuntUnderCurrentLimit.word[0]; break; } - case 29: + case 36: { return ShuntUnderCurrentLimit.word[1]; break; } - case 30: + case 37: { //Shunt Over POWER LIMIT PowerLimit.dblvalue = (uint16_t)i2c_readword(INA_REGISTER::PWR_LIMIT); @@ -1274,20 +1349,20 @@ uint16_t ReadHoldingRegister(uint16_t address) return PowerLimit.word[0]; break; } - case 31: + case 38: { return PowerLimit.word[1]; break; } - case 32: + case 39: { //Shunt Temperature Coefficient return (uint16_t)i2c_readword(INA_REGISTER::SHUNT_TEMPCO); break; } - case 33: + case 40: { //INAXXX chip model number (should always be 0x0228) uint16_t dieid = i2c_readword(INA_REGISTER::DIE_ID); @@ -1298,96 +1373,96 @@ uint16_t ReadHoldingRegister(uint16_t address) //These settings would probably be better in a 0x2B function code //https://modbus.org/docs/Modbus_Application_Protocol_V1_1b.pdf - case 34: + case 41: { //GITHUB version return GIT_VERSION_B1; break; } - case 35: + case 42: { //GITHUB version return GIT_VERSION_B2; break; } - case 36: + case 43: { //COMPILE_DATE_TIME_EPOCH uint32_t x = COMPILE_DATE_TIME_UTC_EPOCH >> 16; return (uint16_t)x; break; } - case 37: + case 44: { //COMPILE_DATE_TIME_EPOCH return (uint16_t)COMPILE_DATE_TIME_UTC_EPOCH; break; } - case 38: + case 45: { //Watchdog timer trigger count (like error counter) return wdt_triggered_count; break; } - case 39: + case 46: { //40040 return i2c_readword(INA_REGISTER::CONFIG); break; } - case 40: + case 47: { return i2c_readword(INA_REGISTER::ADC_CONFIG); break; } - case 41: + case 48: { return i2c_readword(INA_REGISTER::SHUNT_CAL); break; } - case 42: + case 49: { return i2c_readword(INA_REGISTER::SHUNT_TEMPCO); break; } - case 43: + case 50: { return i2c_readword(INA_REGISTER::DIAG_ALRT); break; } - case 44: + case 51: { return i2c_readword(INA_REGISTER::SOVL); break; } - case 45: + case 52: { return i2c_readword(INA_REGISTER::SUVL); break; } - case 46: + case 53: { return i2c_readword(INA_REGISTER::BOVL); break; } - case 47: + case 54: { return i2c_readword(INA_REGISTER::BUVL); break; } - case 48: + case 55: { return i2c_readword(INA_REGISTER::TEMP_LIMIT); break; } - case 49: + case 56: { return i2c_readword(INA_REGISTER::PWR_LIMIT); break; } - case 50: + case 57: { return i2c_readword(INA_REGISTER::DIETEMP); break; From 463a32734c4ac048115c1c58cd4f81a4e32f5947 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Tue, 17 Aug 2021 16:42:00 +0100 Subject: [PATCH 2/6] Variables to hold SOC --- Code/diybmsCurrentShunt/src/main.cpp | 47 +++++++++++++++++++++------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/Code/diybmsCurrentShunt/src/main.cpp b/Code/diybmsCurrentShunt/src/main.cpp index b47fe38..9b3b305 100644 --- a/Code/diybmsCurrentShunt/src/main.cpp +++ b/Code/diybmsCurrentShunt/src/main.cpp @@ -140,6 +140,11 @@ struct eeprom_regs double full_scale_current; double CURRENT_LSB; double RSHUNT; + + uint16_t batterycapacity_amphour; + double fully_charged_voltage; + double tail_current_amps; + double charge_efficiency_factor; }; eeprom_regs registers; @@ -149,6 +154,7 @@ uint16_t alert = 0; volatile bool wdt_triggered = false; volatile uint16_t wdt_triggered_count; +volatile uint16_t SOC; void ConfigurePorts() { @@ -581,7 +587,6 @@ bool SetRegister(uint16_t address, uint16_t value) //Set CONFIG and ADC_CONFIG SetINA228ConfigurationRegisters(); - break; } @@ -602,29 +607,36 @@ bool SetRegister(uint16_t address, uint16_t value) case 20: { //|40021|Battery Capacity (ah) (unsigned int16) + registers.batterycapacity_amphour = value; break; } case 22: { //|40023|Fully charged voltage + newvalue.word[1] = value; + registers.fully_charged_voltage = newvalue.dblvalue; break; } case 24: { //|40025|Tail current (Amps) + newvalue.word[1] = value; + registers.tail_current_amps = newvalue.dblvalue; break; } case 25: { //|40026|Charge efficiency factor % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 9561 = 95.61%) + registers.charge_efficiency_factor = ((double)value) / 100.0; break; } - case 26: - { + //case 26: + //{ //|40027|State of charge % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 8012 = 80.12%, 100 = 1.00%) - break; - } + //registers.batterycapacity_amphour = value; + //break; + //} case 27: { @@ -889,6 +901,11 @@ void setup() registers.shunt_max_current = 150; registers.shunt_millivolt = 50; + registers.batterycapacity_amphour = 200; + registers.fully_charged_voltage = 52.8; + registers.tail_current_amps = 5; + registers.charge_efficiency_factor = 99.5; + //SLOWALERT = Wait for full sample averaging time before triggering alert (about 1.5 seconds) registers.R_DIAG_ALRT = bit(DIAG_ALRT_FIELD::SLOWALERT); @@ -913,6 +930,8 @@ void setup() registers.relay_trigger_bitmap = ALL_ALERT_BITS; } + SOC = 100 * 100; + //Flash LED to indicate normal boot up for (size_t i = 0; i < 6; i++) { @@ -1078,6 +1097,8 @@ uint16_t ReadHoldingRegister(uint16_t address) static DoubleUnionType copy_current_lsb; static DoubleUnionType copy_shunt_resistance; + static DoubleUnionType copy_fully_charged_voltage; + static DoubleUnionType copy_tail_current_amps; switch (address) { @@ -1224,43 +1245,45 @@ uint16_t ReadHoldingRegister(uint16_t address) case 20: { //|40021|Battery Capacity (ah) (unsigned int16) - return 0; + return registers.batterycapacity_amphour; break; } case 21: { //|40022|Fully charged voltage (4 byte double) - return 0; + copy_fully_charged_voltage.dblvalue = registers.fully_charged_voltage; + return copy_fully_charged_voltage.word[0]; break; } case 22: { //|40023|Fully charged voltage - return 0; + return copy_fully_charged_voltage.word[1]; break; } case 23: { //|40024|Tail current (Amps) (4 byte double) - return 0; + copy_tail_current_amps.dblvalue = registers.tail_current_amps; + return copy_tail_current_amps.word[0]; break; } case 24: { //|40025|Tail current (Amps) - return 0; + return copy_tail_current_amps.word[1]; break; } case 25: { //|40026|Charge efficiency factor % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 9561 = 95.61%) - return 0; + return (uint16_t)(registers.charge_efficiency_factor * 100.0); break; } case 26: { //|40027|State of charge % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 8012 = 80.12%, 100 = 1.00%) - return 0; + return SOC; break; } case 27: From 8cd19c1aaf205fdda5de37faa4f46ef44c824bf4 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Tue, 17 Aug 2021 18:50:56 +0100 Subject: [PATCH 3/6] Update MODBUS Registers.md --- Code/diybmsCurrentShunt/MODBUS Registers.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Code/diybmsCurrentShunt/MODBUS Registers.md b/Code/diybmsCurrentShunt/MODBUS Registers.md index a032c58..3e17280 100644 --- a/Code/diybmsCurrentShunt/MODBUS Registers.md +++ b/Code/diybmsCurrentShunt/MODBUS Registers.md @@ -55,10 +55,10 @@ All registers are read only, unless also specified in "Write Registers" later on |40043|GITHUB version |40044|COMPILE_DATE_TIME_EPOCH |40045|COMPILE_DATE_TIME_EPOCH -|40046|Watchdog timer trigger count (like error counter) -|40047|DEBUG CONFIG -|40048|DEBUG ADC_CONFIG -|40049|DEBUG SHUNT_CAL +|40046|Watchdog timer trigger count (like error counter)(unsigned int16) +|40047|DEBUG CONFIG (unsigned int16) +|40048|DEBUG ADC_CONFIG (unsigned int16) +|40049|DEBUG SHUNT_CAL (unsigned int16) |40050|DEBUG SHUNT_TEMPCO |40051|DEBUG DIAG_ALRT |40052|DEBUG SOVL From 23c25a70e78acb27d4d538c7dbf74c691946df1f Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Wed, 18 Aug 2021 10:33:07 +0100 Subject: [PATCH 4/6] Added SOC calculation based on amp hour counting --- Code/diybmsCurrentShunt/src/main.cpp | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/Code/diybmsCurrentShunt/src/main.cpp b/Code/diybmsCurrentShunt/src/main.cpp index 9b3b305..2621830 100644 --- a/Code/diybmsCurrentShunt/src/main.cpp +++ b/Code/diybmsCurrentShunt/src/main.cpp @@ -156,6 +156,27 @@ volatile bool wdt_triggered = false; volatile uint16_t wdt_triggered_count; volatile uint16_t SOC; +uint16_t CalculateSOC() +{ + uint32_t milliamphour_out = charge_c_out * CoulombsToMilliAmpHours; + uint32_t milliamphour_in = ((charge_c_in * CoulombsToMilliAmpHours) / 100 * registers.charge_efficiency_factor); + + //Allow negative numbers + int32_t milliamphour_batterycapacity = 1000 * registers.batterycapacity_amphour; + int32_t difference = milliamphour_in - milliamphour_out; + + //Store result as fixed float point decimal + uint16_t SOC = 10000 * ((milliamphour_batterycapacity + difference) / milliamphour_batterycapacity); + + //Add a hard limit, so user understands that the configuration needs review/change + if (SOC > 10500) + { + SOC = 10500; + } + + return SOC; +} + void ConfigurePorts() { // PA1 = SDA @@ -930,6 +951,7 @@ void setup() registers.relay_trigger_bitmap = ALL_ALERT_BITS; } + //100.00% at power on SOC = 100 * 100; //Flash LED to indicate normal boot up @@ -1021,7 +1043,6 @@ double TemperatureLimit() { //Case unsigned to int16 to cope with negative temperatures double temp = (int16_t)i2c_readword(INA_REGISTER::TEMP_LIMIT); - return temp * (double)0.0078125; } @@ -1283,6 +1304,7 @@ uint16_t ReadHoldingRegister(uint16_t address) case 26: { //|40027|State of charge % (unsigned int16) (scale x100 eg. 10000 = 100.00%, 8012 = 80.12%, 100 = 1.00%) + SOC = CalculateSOC(); return SOC; break; } From 20d379cf7e33bb7f5fee093b0a12311c8ff71b7d Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Wed, 18 Aug 2021 16:59:31 +0100 Subject: [PATCH 5/6] Changed mAh counting logic to use more integer math --- Code/diybmsCurrentShunt/src/main.cpp | 154 ++++++++++++++++----------- 1 file changed, 94 insertions(+), 60 deletions(-) diff --git a/Code/diybmsCurrentShunt/src/main.cpp b/Code/diybmsCurrentShunt/src/main.cpp index 2621830..9019ea1 100644 --- a/Code/diybmsCurrentShunt/src/main.cpp +++ b/Code/diybmsCurrentShunt/src/main.cpp @@ -33,6 +33,9 @@ Attribution-NonCommercial-ShareAlike 2.0 UK: England & Wales (CC BY-NC-SA 2.0 UK // MODBUS Protocol // https://www.ni.com/en-gb/innovations/white-papers/14/the-modbus-protocol-in-depth.html +//INA228 85-V, 20-Bit, Ultra-Precise Power/Energy/Charge Monitor +//https://www.ti.com/lit/ds/symlink/ina228.pdf + #if !defined(MODBUSDEFAULTBAUDRATE) #error MODBUSDEFAULTBAUDRATE must be defined #endif @@ -96,8 +99,10 @@ const double full_scale_adc = 40.96; const double CoulombsToMilliAmpHours = 1.0 / 3.6; const uint8_t INA228_I2C_Address = B1000000; -double charge_c_in = 0; -double charge_c_out = 0; +const uint16_t loop_delay_ms = 2000; + +uint32_t milliamphour_out = 0; +uint32_t milliamphour_in = 0; uint16_t ModBusBaudRate = MODBUSDEFAULTBAUDRATE; @@ -106,6 +111,9 @@ uint8_t ModbusSlaveAddress = MODBUSBASEADDRESS; volatile bool relay_state = false; volatile bool config_dirty = false; +int32_t last_charge_coulombs = 0; +uint16_t soc_reset_counter = 0; + unsigned long timer = 0; #define combineBytes(high, low) (high << 8) + low @@ -158,12 +166,10 @@ volatile uint16_t SOC; uint16_t CalculateSOC() { - uint32_t milliamphour_out = charge_c_out * CoulombsToMilliAmpHours; - uint32_t milliamphour_in = ((charge_c_in * CoulombsToMilliAmpHours) / 100 * registers.charge_efficiency_factor); + double milliamphour_in_scaled = ((double)milliamphour_in / 100.0) * registers.charge_efficiency_factor; + double milliamphour_batterycapacity = 1000.0 * (uint32_t)registers.batterycapacity_amphour; - //Allow negative numbers - int32_t milliamphour_batterycapacity = 1000 * registers.batterycapacity_amphour; - int32_t difference = milliamphour_in - milliamphour_out; + double difference = milliamphour_in_scaled - milliamphour_out; //Store result as fixed float point decimal uint16_t SOC = 10000 * ((milliamphour_batterycapacity + difference) / milliamphour_batterycapacity); @@ -546,7 +552,7 @@ bool SetRegister(uint16_t address, uint16_t value) newvalue.word[0] = value; break; } - + /* case 5: { //amphour_out @@ -564,7 +570,7 @@ bool SetRegister(uint16_t address, uint16_t value) charge_c_in = x * CoulombsToMilliAmpHours; break; } - +*/ case 9: { // Bit flags @@ -951,8 +957,8 @@ void setup() registers.relay_trigger_bitmap = ALL_ALERT_BITS; } - //100.00% at power on - SOC = 100 * 100; + //0% at power on + SOC = 0; //Flash LED to indicate normal boot up for (size_t i = 0; i < 6; i++) @@ -990,16 +996,17 @@ void setup() modbus_configure(&Serial, ModBusBaudRate); } +//Bus voltage output. Two's complement value, however always positive. Value in bits 23 to 4 double BusVoltage() { - //Bus voltage output. Two's complement value, however always positive. Value in bits 23 to 4 + //195.3125uV per LSB return (double)195.3125 * (double)i2c_readInt24(INA_REGISTER::VBUS) / 1000000.0; } +//Shunt voltage in MILLIVOLTS mV (Two's complement value) double ShuntVoltage() { - //Shunt voltage in MILLIVOLTS mV (Two's complement value) //78.125 nV/LSB return (double)78.125 * (double)i2c_readInt24(INA_REGISTER::VSHUNT) / 1e+6; } @@ -1012,24 +1019,24 @@ double Energy() } // Charge in Coulombs -int64_t RawCharge() +int32_t ChargeInCoulombsAsInt() { - return i2c_readInt40(INA_REGISTER::CHARGE); + //Calculated charge output. Output value is in Coulombs.Two's complement value. 40bit number + //int64 on an 8 bit micro! + return registers.CURRENT_LSB * (double)i2c_readInt40(INA_REGISTER::CHARGE); } +//Calculated power output. Output value in watts. Unsigned representation. Positive value. double Power() { - //Calculated power output. - //Output value in watts. - //Unsigned representation. Positive value. //POWER Power [W] = 3.2 x CURRENT_LSB x POWER return (double)i2c_readUint24(INA_REGISTER::POWER) * (double)3.2 * registers.CURRENT_LSB; } +//The INA228 device has an internal temperature sensor which can measure die temperature from –40 °C to +125°C. double DieTemperature() { - //The INA228 device has an internal temperature sensor which can measure die temperature from –40 °C to +125 - //°C. The accuracy of the temperature sensor is ±2 °C across the operational temperature range. The temperature + //The accuracy of the temperature sensor is ±2 °C across the operational temperature range. The temperature //value is stored inside the DIETEMP register and can be read through the digital interface //Internal die temperature measurement. Two's complement value. Conversion factor: 7.8125 m°C/LSB @@ -1046,11 +1053,13 @@ double TemperatureLimit() return temp * (double)0.0078125; } +//Calculated current output in Amperes. +//In the way this circuit is designed, NEGATIVE current indicates DISCHARGE of the battery +//POSITIVE current indicates CHARGE of the battery double Current() { - //Current. - //Calculated current output in Amperes. Two's complement value. - return registers.CURRENT_LSB * (double)i2c_readInt24(INA_REGISTER::CURRENT); + //Current. Two's complement value. + return -(registers.CURRENT_LSB * (double)i2c_readInt24(INA_REGISTER::CURRENT)); } ISR(PORTB_PORT_vect) @@ -1113,9 +1122,6 @@ uint16_t ReadHoldingRegister(uint16_t address) static DoubleUnionType ShuntUnderCurrentLimit; static DoubleUnionType PowerLimit; - static uint32_t milliamphour_out; - static uint32_t milliamphour_in; - static DoubleUnionType copy_current_lsb; static DoubleUnionType copy_shunt_resistance; static DoubleUnionType copy_fully_charged_voltage; @@ -1156,7 +1162,6 @@ uint16_t ReadHoldingRegister(uint16_t address) case 4: { //milliamphour_out - milliamphour_out = charge_c_out * CoulombsToMilliAmpHours; return (uint16_t)(milliamphour_out >> 16); break; } @@ -1171,7 +1176,6 @@ uint16_t ReadHoldingRegister(uint16_t address) case 6: { //milliamphour_in - milliamphour_in = charge_c_in * CoulombsToMilliAmpHours; return (uint16_t)(milliamphour_in >> 16); break; } @@ -1518,8 +1522,6 @@ uint16_t ReadHoldingRegister(uint16_t address) return 0; } -double last_charge_coulombs = 0; - void loop() { wdt_reset(); @@ -1584,53 +1586,85 @@ void loop() RedLED(true); - //Do it again in 2.5 seconds - timer = millis() + 2500; - - //double voltage = BusVoltage(); - //150A@50mV shunt = 122.88A @ 40.96mV (full scale ADC) - //double current = Current(); - - //Power is always a positive number - double power = Power(); - //double temperature = DieTemperature(); - //double shuntv = ShuntVoltage(); - //double energy_joules = Energy(); - - //int64 on an 8 bit micro! - int64_t charge_raw = RawCharge(); + //Do it again in X seconds + timer = millis() + loop_delay_ms; - double charge_coulombs = registers.CURRENT_LSB * charge_raw; + double voltage = BusVoltage(); + double current = Current(); - double difference = charge_coulombs - last_charge_coulombs; + //We amp-hour count using units of 18 coulombs = 5mAh, to avoid rounding issues - //amphour_out = charge_coulombs * CoulombsToAmpHours; - - //If we don't have a power reading, ignore the coulombs - also means + //If we don't have a voltage reading, ignore the coulombs - also means //Ah counting won't work without voltage reading on the INA228 chip - if (power > 0) + if (voltage > 0) { - if (difference > 0) - { - charge_c_out += difference; - } - else + int32_t charge_coulombs = ChargeInCoulombsAsInt(); + int32_t difference = charge_coulombs - last_charge_coulombs; + + //Have we used up more than 5mAh of energy? + //if not, ignore for now and await next cycle + if (abs(difference) >= 18) { - charge_c_in += abs(difference); + if (difference > 0) + { + //Amp hour out + //Integer divide (18 coulombs) + int32_t integer_divide = (difference / 18); + //Subtract remainder + last_charge_coulombs = charge_coulombs - (difference - (integer_divide * 18)); + //Chunks of 5mAh + milliamphour_out += integer_divide * 5; + } + else + { + //Make it positive, for counting amp hour in + difference = abs(difference); + int32_t integer_divide = (difference / 18); + //Add on remainder + last_charge_coulombs = charge_coulombs + (difference - (integer_divide * 18)); + //chunks of 5mAh + milliamphour_in += integer_divide * 5; + } } } - last_charge_coulombs = charge_coulombs; - //Periodically we need to reset the energy register to prevent it overflowing //if we do this too frequently we get incorrect readings over the long term //360000 = 100Amp Hour - if (last_charge_coulombs > 360000.0) + if (last_charge_coulombs > 360000) { ResetChargeEnergyRegisters(); last_charge_coulombs = 0; } + //Now to test if we need to reset SOC to 100% ? + //Check if voltage is over the fully_charged_voltage and current UNDER tail_current_amps + if (voltage >= registers.fully_charged_voltage && current < registers.tail_current_amps && current > 0) + { + //Fully charged, waiting condition is true so increment time count + soc_reset_counter++; + + // Test if counter has reached 3 minutes, indicating fully charge battery + if (soc_reset_counter >= ((3 * 60 * 1000) / loop_delay_ms)) + { + //Now we reset the SOC, by clearing the registers, at this point SOC returns to 100% + + //This does have an annoying "feature" of clearing down todays AH counts :-( + //TODO: FIX THIS - probably need a set of shadow variables to hold the internal SOC and AH counts + + ResetChargeEnergyRegisters(); + last_charge_coulombs = 0; + milliamphour_in = 0; + milliamphour_out = 0; + soc_reset_counter = 0; + } + } + else + { + //Voltage or current is out side of monitoring limits, so reset timer count + soc_reset_counter = 0; + } + if (alert == 0) { //Turn LED off if alert is not active From 3b2bc6f6a397ac331fd71431c515296a85d5a526 Mon Sep 17 00:00:00 2001 From: Stuart Pittaway <1201909+stuartpittaway@users.noreply.github.com> Date: Thu, 19 Aug 2021 14:35:47 +0100 Subject: [PATCH 6/6] Fix issue with resetting coulomb counter --- Code/diybmsCurrentShunt/src/main.cpp | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/Code/diybmsCurrentShunt/src/main.cpp b/Code/diybmsCurrentShunt/src/main.cpp index 9019ea1..5ff3d69 100644 --- a/Code/diybmsCurrentShunt/src/main.cpp +++ b/Code/diybmsCurrentShunt/src/main.cpp @@ -111,8 +111,9 @@ uint8_t ModbusSlaveAddress = MODBUSBASEADDRESS; volatile bool relay_state = false; volatile bool config_dirty = false; +uint8_t max_soc_reset_counter = 0; +uint8_t soc_reset_counter = 0; int32_t last_charge_coulombs = 0; -uint16_t soc_reset_counter = 0; unsigned long timer = 0; @@ -230,7 +231,6 @@ void ReadJumperPins() bool AddressJumper = (PORTA.IN & PIN4_bm); //TODO: Do something with the above jumper settings - if (BaudRateJumper == false) { //Half the default baud rate if the jumper is connected, so 19200 goes to 9600 @@ -481,11 +481,12 @@ void __attribute__((noreturn)) blinkPattern(uint32_t pattern) void ResetChargeEnergyRegisters() { //BIT 14 + //RSTACC //Resets the contents of accumulation registers ENERGY and CHARGE to 0 //0h = Normal Operation //1h = Clears registers to default values for ENERGY and CHARGE registers - if (!i2c_writeword(INA_REGISTER::CONFIG, registers.R_CONFIG | 0x1000)) + if (!i2c_writeword(INA_REGISTER::CONFIG, registers.R_CONFIG | (uint16_t)_BV(14))) { blinkPattern(err_ResetChargeEnergyRegisters); } @@ -688,7 +689,6 @@ bool SetRegister(uint16_t address, uint16_t value) //BusOverVolt.dblvalue = ((double)(uint16_t)i2c_readword(INA_REGISTER::BOVL)) * 0.003125F; newvalue.word[1] = value; registers.R_BOVL = newvalue.dblvalue / 0.003125F; - //TODO: Save the new value break; } @@ -1018,7 +1018,10 @@ double Energy() return 16.0 * 3.2 * registers.CURRENT_LSB * energy; } -// Charge in Coulombs +// Charge in Coulombs. +// The raw value is 40 bit, but we frequently reset this back to zero so it prevents +// overflow and keeps inside the int32 limits +// NEGATIVE value means battery is being charged int32_t ChargeInCoulombsAsInt() { //Calculated charge output. Output value is in Coulombs.Two's complement value. 40bit number @@ -1631,7 +1634,7 @@ void loop() //Periodically we need to reset the energy register to prevent it overflowing //if we do this too frequently we get incorrect readings over the long term //360000 = 100Amp Hour - if (last_charge_coulombs > 360000) + if (abs(last_charge_coulombs) > (int32_t)360000) { ResetChargeEnergyRegisters(); last_charge_coulombs = 0; @@ -1639,19 +1642,21 @@ void loop() //Now to test if we need to reset SOC to 100% ? //Check if voltage is over the fully_charged_voltage and current UNDER tail_current_amps - if (voltage >= registers.fully_charged_voltage && current < registers.tail_current_amps && current > 0) + if (voltage > registers.fully_charged_voltage && current > 0 && current < registers.tail_current_amps) { - //Fully charged, waiting condition is true so increment time count + //Battery has reached fully charged so wait for time counter soc_reset_counter++; // Test if counter has reached 3 minutes, indicating fully charge battery - if (soc_reset_counter >= ((3 * 60 * 1000) / loop_delay_ms)) + if (soc_reset_counter >= ((3 * 60) / (loop_delay_ms/1000))) { //Now we reset the SOC, by clearing the registers, at this point SOC returns to 100% //This does have an annoying "feature" of clearing down todays AH counts :-( //TODO: FIX THIS - probably need a set of shadow variables to hold the internal SOC and AH counts + // but then when/how do we reset the Ah counts? + max_soc_reset_counter = soc_reset_counter; ResetChargeEnergyRegisters(); last_charge_coulombs = 0; milliamphour_in = 0; @@ -1669,6 +1674,7 @@ void loop() { //Turn LED off if alert is not active RedLED(false); - } - } + } //end if + + } //end if } \ No newline at end of file