stuartpittaway / diyBMSv4ESP32

diyBMS v4 code for the ESP32 and new controller hardware
Other
182 stars 80 forks source link

INA 229 , changing from adc_range 1 to adc_range0 for 75mV shunts. #214

Open lorenzo6201 opened 1 year ago

lorenzo6201 commented 1 year ago

Like I mentioned in the forum I would like to use 75mV shunts with ina229 addon board. I have tried to alter CurrentMonitorINA229.cpp and CurrentMonitorINA229.h It works but not precise. The value is higher or lower than real. I think I missed something or maybe is due to scaling to 50mV shunts. i looked several times over the code , used the TI ina229 calculator but I have no real idea where to put the values. I will copy paste the code here

CurrentMonitorINA229.cpp `/*


( ( )( \/ )( ( \/ )/ ) ( \/ )/. | )() ))( \ / ) < ) ( _ \ \ /( _) (/() () (__/(/\/_)(/ \/ (_)

DIYBMS V4.0 INA229 CURRENT/ENERGY MONITOR CHIP (SPI INTERFACE)

(c)2023 Stuart Pittaway

COMPILE THIS CODE USING PLATFORM.IO LICENSE Attribution-NonCommercial-ShareAlike 2.0 UK: England & Wales (CC BY-NC-SA 2.0 UK) https://creativecommons.org/licenses/by-nc-sa/2.0/uk/

COMMERCIAL USE AND RESALE PROHIBITED */

define USE_ESP_IDF_LOG 1

static constexpr const char *const TAG = "curmon";

include "CurrentMonitorINA229.h"

void CurrentMonitorINA229::CalculateLSB() { // Take a look at these for information on how it works! // https://dev.ti.com/gallery/view/4910879/INA228_229_237_238_239EVM_GUI/ver/2.0.0/ // in above - click COG icon top left. // https://e2e.ti.com/support/amplifiers-group/amplifiers/f/amplifiers-forum/1034569/ina228-accumulated-energy-and-charge-is-wrong

// 150A/50mV shunt =   full_scale_current= 150.00A / 50.00 * 40.96 = 122.88 AMPS
//                     RSHUNT = (50 / 1000) / 150 = 0.00033333333
//                     CURRENT_LSB = 150/ 524288 = 0.000286102294921875
//                     R_SHUNT_CAL = 52428800000*0.000286102294921875*0.00033333333 = 4999.999 = 5000

// 300A/75mV shunt =   full_scale_current= 300.00A / 75.00 * 40.96 = 163.84 AMPS
//                     RSHUNT = (75 / 1000) / 163.84 = 0.000457763671875
//                     CURRENT_LSB = 163.84/ 524288 = 2.648162841796875e-4
//                     R_SHUNT_CAL = 52428800000*0.00057220458984375*0.00025 = 7499.9922688 = 7500

// Calculate CURRENT_LSB and R_SHUNT_CAL values
// registers.full_scale_current = ((float)registers.shunt_max_current / (float)registers.shunt_millivolt) * full_scale_adc;
registers.RSHUNT = ((float)registers.shunt_millivolt / 1000.0) / (float)registers.shunt_max_current;
registers.CURRENT_LSB = registers.shunt_max_current / (float)0x80000;
registers.R_SHUNT_CAL = 4L * (13107200000L * registers.CURRENT_LSB * registers.RSHUNT);

ESP_LOGI(TAG, "RSHUNT=%.8f, CURRENT_LSB=%.8f, R_SHUNT_CAL=%u", registers.RSHUNT, registers.CURRENT_LSB, registers.R_SHUNT_CAL);

// Will the full scale current be over the 40.96mV range the ADC can handle?
if (((float)registers.shunt_max_current * registers.RSHUNT * 1000.0) > full_scale_adc)
{
    float true_max_current = ((float)registers.shunt_max_current / (float)registers.shunt_millivolt) * full_scale_adc;
    ESP_LOGW(TAG, "WARNING: True Max Current Measurable %.2f Amps", true_max_current);

    registers.CURRENT_LSB = true_max_current / (float)0x80000;
    registers.R_SHUNT_CAL = 4L * (13107200000L * registers.CURRENT_LSB * registers.RSHUNT);
    ESP_LOGI(TAG, "RECALC: RSHUNT=%.8f, CURRENT_LSB=%.8f, R_SHUNT_CAL=%u", registers.RSHUNT, registers.CURRENT_LSB, registers.R_SHUNT_CAL);
}

// Deliberately reduce calibration by 2.5%, which appears to be the loses seen in the current monitor circuit design
// (or shunt resistance tolerance)
// You can always configure this value through the web gui - "Calibration" value.
// registers.R_SHUNT_CAL = ((uint32_t)registers.R_SHUNT_CAL * 985) / 1000;

}

// Sets SOC by setting "fake" in/out amphour counts // value=8212 = 82.12% void CurrentMonitorINA229::SetSOC(uint16_t value) { // Assume battery is fully charged milliamphour_in = 1000 (uint32_t)registers.batterycapacity_amphour; // And we have consumed this much... milliamphour_out = (1.0 - ((float)value / 10000.0)) milliamphour_in;

// Zero out readings using the offsets
milliamphour_out_offset = milliamphour_out;
milliamphour_in_offset = milliamphour_in;

}

uint8_t CurrentMonitorINA229::readRegisterValue(INA_REGISTER r) { // These are not really registers, but shape the SPI frame to indicate read/write // page 19 of documentation, Table 7-2. First 8-MSB Bits of SPI Frame. // Read register return (r << 2) | B00000001; } uint8_t CurrentMonitorINA229::writeRegisterValue(INA_REGISTER r) { // These are not really registers, but shape the SPI frame to indicate read/write // page 19 of documentation, Table 7-2. First 8-MSB Bits of SPI Frame. // Write register return (r << 2) | B00000000; }

uint16_t CurrentMonitorINA229::read16bits(INA_REGISTER r) { SPI_Ptr->beginTransaction(_spisettings); digitalWrite(chipselectpin, LOW); // The transfers are always a step behind, so the transfer reads the previous value/command SPI_Ptr->write(readRegisterValue(r)); uint16_t value = SPI_Ptr->transfer16(0); digitalWrite(chipselectpin, HIGH); SPI_Ptr->endTransaction();

// ESP_LOGD(TAG, "Read register 0x%02x = 0x%04x", r, value);
return value;

}

// Write a register in INA229 // During an SPI write frame, while new data is shifted into the INA229-Q1 // register, the old data from the same register is shifted out on the MISO line. uint16_t CurrentMonitorINA229::write16bits(INA_REGISTER r, uint16_t value) { SPI_Ptr->beginTransaction(_spisettings); digitalWrite(chipselectpin, LOW); // The transfers are always a step behind, so the transfer reads the previous value/command SPI_Ptr->write(writeRegisterValue(r)); uint16_t retvalue = SPI_Ptr->transfer16(value); digitalWrite(chipselectpin, HIGH); SPI_Ptr->endTransaction();

// ESP_LOGD(TAG, "Write register 0x%02x = 0x%04x (old value=0x%04x)", r, value, retvalue);
//  retvalue is the PREVIOUS value stored in the register
return retvalue;

}

// Reads 3 bytes from SPI bus, and returns them in a uint32 (lower 24 bits) uint32_t CurrentMonitorINA229::spi_readUint24(INA_REGISTER r) { SPI_Ptr->beginTransaction(_spisettings); digitalWrite(chipselectpin, LOW); SPI_Ptr->write(readRegisterValue(r)); uint8_t a = SPI_Ptr->transfer(0); uint8_t b = SPI_Ptr->transfer(0); uint8_t c = SPI_Ptr->transfer(0); digitalWrite(chipselectpin, HIGH); SPI_Ptr->endTransaction();

uint32_t value = ((uint32_t)a << 16) | ((uint32_t)b << 8) | ((uint32_t)c);

// ESP_LOGD(TAG, "Read register 0x%02x = 0x%08x", r, value);
return value;

}

int64_t CurrentMonitorINA229::spi_readInt40(INA_REGISTER r) { SPI_Ptr->beginTransaction(_spisettings); digitalWrite(chipselectpin, LOW); SPI_Ptr->write(readRegisterValue(r)); uint8_t a = SPI_Ptr->transfer(0); uint8_t b = SPI_Ptr->transfer(0); uint8_t c = SPI_Ptr->transfer(0); uint8_t d = SPI_Ptr->transfer(0); uint8_t e = SPI_Ptr->transfer(0); digitalWrite(chipselectpin, HIGH); SPI_Ptr->endTransaction();

// Check if a twos compliment negative number
uint64_t reply = (a & 0x80) ? (uint64_t)0xFFFFFF0000000000 : 0;

reply += (uint64_t)a << 32;
reply += (uint64_t)b << 24;
reply += (uint64_t)c << 16;
reply += (uint64_t)d << 8;
reply += (uint64_t)e;

// ESP_LOGD(TAG, "Read register 0x%02x");

// Cast to signed integer (which also sorts out the negative sign if applicable)
return (int64_t)reply;

}

uint64_t CurrentMonitorINA229::spi_readUint40(INA_REGISTER r) { SPI_Ptr->beginTransaction(_spisettings); digitalWrite(chipselectpin, LOW); SPI_Ptr->write(readRegisterValue(r)); uint8_t a = SPI_Ptr->transfer(0); uint8_t b = SPI_Ptr->transfer(0); uint8_t c = SPI_Ptr->transfer(0); uint8_t d = SPI_Ptr->transfer(0); uint8_t e = SPI_Ptr->transfer(0); digitalWrite(chipselectpin, HIGH); SPI_Ptr->endTransaction();

uint64_t value = (uint64_t)a << 32;
value += (uint64_t)b << 24;
value += (uint64_t)c << 16;
value += (uint64_t)d << 8;
value += (uint64_t)e;

// ESP_LOGD(TAG, "Read register 0x%02x", r);
return value;

}

// Read a 24 bit (3 byte) unsigned integer into a uint32 (including right shift 4 bits) uint32_t CurrentMonitorINA229::readUInt24(INA_REGISTER r) { return spi_readUint24(r) >> 4; }

// Energy in JOULES float CurrentMonitorINA229::Energy() { uint64_t energy = spi_readUint40(INA_REGISTER::ENERGY); return 16.0 3.2 registers.CURRENT_LSB * energy; }

// 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 CurrentMonitorINA229::ChargeInCoulombsAsInt() { // Calculated charge output. Output value is in Coulombs.Two's complement value. 40bit number stored in int64 return registers.CURRENT_LSB * (float)spi_readInt40(INA_REGISTER::CHARGE); }

// Bus voltage output. Two's complement value, however always positive. Value in bits 23 to 4 float CurrentMonitorINA229::BusVoltage() { uint32_t busVoltage = readUInt24(INA_REGISTER::VBUS) & 0x000FFFFF; // The accuracy is 20bits and 195.3125uV is the LSB uint32_t busVoltage_mV = (uint64_t)busVoltage * (uint64_t)0x1DCD65 / (uint64_t)0x989680; // conversion to get mV

// ESP_LOGD(TAG, "busVoltage mV=%u", busVoltage_mV);

//  Return VOLTS
return (float)busVoltage_mV / (float)1000.0;

}

// Read a 20 bit (3 byte) TWOS COMPLIMENT integer from an INA register // Used to read VSHUNT value int32_t CurrentMonitorINA229::readInt20(INA_REGISTER r) { // Returns uint32_t value = readUInt24(r);

// The number is two's complement, check for negative at bit 20
if (value & 0x80000)
{
    // Invert
    value = ~value;
    // Trim to 20 bits
    value = value & 0x000FFFFFU;
    // Add 1
    value = value + 1;
    // Return NEGATIVE of the value
    return -(int32_t)value;
}

return (int32_t)value;

}

// Shunt voltage in MILLIVOLTS mV float CurrentMonitorINA229::ShuntVoltage() { // 78.125 nV/LSB when ADCRANGE = 1 // Differential voltage measured across the shunt output. Two's complement value. // 20 bit value max = 1048575 int32_t vshunt = readInt20(INA_REGISTER::VSHUNT); return (float)(((int64_t)vshunt) * 312500UL) / 1000000000.0; }

void CurrentMonitorINA229::TakeReadings() { voltage = BusVoltage(); current = Current(); power = Power(); temperature = DieTemperature(); SOC = CalculateSOC();

CalculateAmpHourCounts();

ESP_LOGD(TAG, "V=%.4f, I=%.4f, P=%.2f", voltage, current, power);

}

uint16_t CurrentMonitorINA229::CalculateSOC() { float milliamphour_in_scaled = ((float)milliamphour_in / 100.0) registers.charge_efficiency_factor; float milliamphour_batterycapacity = 1000.0 (uint32_t)registers.batterycapacity_amphour; float difference = milliamphour_in_scaled - milliamphour_out;

float answer = 100 * (difference / milliamphour_batterycapacity);
if (answer < 0)
{
    // We have taken more out of the battery than put in, so must be zero SoC (or more likely out of calibration)
    return 0;
}

// Store result as fixed point decimal
uint16_t SOC = 100 * answer;

// Add a hard upper limit 999.99%
if (SOC > 99999)
{
    SOC = (uint16_t)99999;
}

return SOC;

}

void CurrentMonitorINA229::CalculateAmpHourCounts() { // We amp-hour count using units of 18 coulombs = 5mAh, to avoid rounding issues

// 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 (voltage > 0)
{
    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)
    {
        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
            uint32_t a = integer_divide * 5;
            milliamphour_out += a;
            milliamphour_out_lifetime += a;
            daily_milliamphour_out += a;
        }
        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
            uint32_t a = integer_divide * 5;
            milliamphour_in += a;
            milliamphour_in_lifetime += a;
            daily_milliamphour_in += a;
        }
    }
}

// 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 (abs(last_charge_coulombs) > (int32_t)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 > 0 && current < registers.tail_current_amps)
{
    // Battery has reached fully charged so wait for time counter to elapse
    ESP_LOGI(TAG,"Battery has reached charging tail period");

    if (esp_timer_get_time() > soc_reset_time)
    {
        // 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 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?
        ResetChargeEnergyRegisters();
        last_charge_coulombs = 0;
        SetSOC(10000);
    }
}
else
{
    // Voltage or current is out side of monitoring limits, so set timer to now + 3 minutes
    int64_t soc_reset_time = esp_timer_get_time() + (3 * 60000000L);
}

}

// Guess the SoC % based on battery voltage - not accurate, just a guess! void CurrentMonitorINA229::GuessSOC() { // Default SOC% at 60% uint16_t soc = 6000;

// We apply a "guestimate" to SoC based on voltage - not really accurate, but somewhere to start
// only applicable to 24V/48V (16S) LIFEPO4 setups. These voltages should be the unloaded (no current flowing) voltage.
// Assumption that its LIFEPO4 cells we are using...
float v = BusVoltage();

if (v > 20 && v < 30)
{
    // Scale up 24V battery to use the 48V scale
    v = v * 2;
}

if (v > 40 && v < 60)
{
    // 16S LIFEPO4...
    if (v >= 40.0)
        soc = 500;
    if (v >= 48.0)
        soc = 900;
    if (v >= 50.0)
        soc = 1400;
    if (v >= 51.2)
        soc = 1700;
    if (v >= 51.6)
        soc = 2000;
    if (v >= 52.0)
        soc = 3000;
    if (v >= 52.4)
        soc = 4000;
    if (v >= 52.8)
        soc = 7000;
    if (v >= 53.2)
        soc = 9000;
}

SetSOC(soc);

// Reset the daily counters
daily_milliamphour_in = 0;
daily_milliamphour_out = 0;

}

bool CurrentMonitorINA229::Configure(uint16_t shuntmv, uint16_t shuntmaxcur, uint16_t batterycapacity, uint16_t fullchargevolt, uint16_t tailcurrent, uint16_t chargeefficiency, uint16_t shuntcal, int16_t temperaturelimit, int16_t overvoltagelimit, int16_t undervoltagelimit, int32_t overcurrentlimit, int32_t undercurrentlimit, int32_t overpowerlimit, uint16_t shunttempcoefficient, bool TemperatureCompEnabled) { registers.shunt_millivolt = shuntmv; registers.shunt_max_current = shuntmaxcur; registers.fully_charged_voltage = fullchargevolt / 100.0; registers.batterycapacity_amphour = batterycapacity; registers.tail_current_amps = tailcurrent / 100.0; registers.charge_efficiency_factor = chargeefficiency / 100.0;

CalculateLSB();

if (shuntcal != 0 && registers.R_SHUNT_CAL != shuntcal)
{
    ESP_LOGI(TAG, "Override SHUNT_CAL from %u to %u", registers.R_SHUNT_CAL, shuntcal);
    // Are we trying to override the default SHUNT_CAL value?
    registers.R_SHUNT_CAL = shuntcal;
}

// This is not enabled by default
// The 16 bit register provides a resolution of 1ppm/°C/LSB
// Shunt Temperature Coefficient - 3FFF is maximum
registers.R_SHUNT_TEMPCO = shunttempcoefficient & 0x3FFF;

// Shunt Over Limit (current limit) = overcurrent protection
registers.R_SOVL = ConvertTo2sComp((((float)overcurrentlimit / 100.0F) * 1000.0F / 1.25F) * full_scale_adc / registers.shunt_max_current);
// Shunt UNDER Limit (under current limit) = undercurrent protection
registers.R_SUVL = ConvertTo2sComp((((float)undercurrentlimit / 100.0F) * 1000.0F / 1.25F) * full_scale_adc / registers.shunt_max_current);
// Bus Overvoltage (overvoltage protection).
registers.R_BOVL = ConvertTo2sComp(((float)overvoltagelimit / 100.0F) / 0.003125F);
// Bus under voltage protection
registers.R_BUVL = ConvertTo2sComp(((float)undervoltagelimit / 100.0F) / 0.003125F);
// temperature limit
registers.R_TEMP_LIMIT = (int16_t)temperaturelimit / (float)0.0078125;

// Default Power limit = 5kW
registers.R_PWR_LIMIT = (uint16_t)(overpowerlimit / 256.0F / 3.2F / registers.CURRENT_LSB);

// ESP_LOGI(TAG, "undercurrentlimit=%f, R_SUVL=%u", ((float)undercurrentlimit / 100.0F), registers.R_SUVL);

registers.R_CONFIG = _BV(4); // ADCRANGE = 40.96mV scale

if (TemperatureCompEnabled)
{
    // Set bit
    registers.R_CONFIG |= bit(5);
}

// Configure other registers
write16bits(INA_REGISTER::CONFIG, registers.R_CONFIG);
write16bits(INA_REGISTER::ADC_CONFIG, registers.R_ADC_CONFIG);
write16bits(INA_REGISTER::SHUNT_CAL, registers.R_SHUNT_CAL);
write16bits(INA_REGISTER::DIAG_ALRT, registers.R_DIAG_ALRT);

// Check MEMSTAT=1 which proves the INA chip is not corrupt
diag_alrt_value = read16bits(INA_REGISTER::DIAG_ALRT);
if (diag_alrt_value & bit(DIAG_ALRT_FIELD::MEMSTAT) == 0)
{
    // MEMSTAT error
    ESP_LOGE(TAG, "INA229 chip MEMSTAT error");
    INA229Installed = false;
    return false;
}

SetINA229Registers();

return true;

}

// Initialise INA229 chip and class object // fullchargevolt, tailcurrent and chargeefficiency are fixed point values (x100, example 1234=12.34) bool CurrentMonitorINA229::Initialise(SPIClass *SPI, uint8_t cs_pin) { SPI_Ptr = SPI; chipselectpin = cs_pin; assert(SPI != NULL);

uint16_t value = read16bits(INA_REGISTER::DEVICE_ID);
if ((value >> 4) == 0x229)
{
    ESP_LOGI(TAG, "Chip=INA%02x, Revision=%u", value >> 4, value & B1111);
    INA229Installed = true;
}
else
{
    // Stop here - no chip found
    ESP_LOGI(TAG, "INA229 chip absent");
    return false;
}

// Now we know the INA chip is connected, reset to power on defaults
// 15=Reset Bit. Setting this bit to '1' generates a system reset that is the same as power-on reset.
write16bits(INA_REGISTER::CONFIG, (uint16_t)_BV(15));
// Allow the reset to work
delay(100);

return true;

}

void CurrentMonitorINA229::SetINA229Registers() { write16bits(INA_REGISTER::SHUNT_CAL, registers.R_SHUNT_CAL); write16bits(INA_REGISTER::SHUNT_TEMPCO, registers.R_SHUNT_TEMPCO); write16bits(INA_REGISTER::SOVL, registers.R_SOVL); write16bits(INA_REGISTER::SUVL, registers.R_SUVL); write16bits(INA_REGISTER::BOVL, registers.R_BOVL); write16bits(INA_REGISTER::BUVL, registers.R_BUVL); write16bits(INA_REGISTER::TEMP_LIMIT, registers.R_TEMP_LIMIT); write16bits(INA_REGISTER::PWR_LIMIT, registers.R_PWR_LIMIT); } ` CurrentMonitorINA229.h

`/*


( ( )( \/ )( ( \/ )/ ) ( \/ )/. | )() ))( \ / ) < ) ( _ \ \ /( _) (/() () (__/(/\/_)(/ \/ (_)

DIYBMS V4.0 INA229 CURRENT/ENERGY MONITOR CHIP (SPI INTERFACE)

(c)2023 Stuart Pittaway

COMPILE THIS CODE USING PLATFORM.IO LICENSE Attribution-NonCommercial-ShareAlike 2.0 UK: England & Wales (CC BY-NC-SA 2.0 UK) https://creativecommons.org/licenses/by-nc-sa/2.0/uk/

COMMERCIAL USE AND RESALE PROHIBITED */

/ DATASHEET INA229-Q1 AEC-Q100, 85-V, 20-Bit, Ultra-Precise Power/Energy/Charge Monitor With SPI Interface https://www.ti.com/lit/ds/symlink/ina229-q1.pdf?ts=1599057970767 /

define CONFIG_DISABLE_HAL_LOCKS 1

pragma once

include

include

ifndef CURRENTMONITORINA229H

define CURRENTMONITORINA229H

class CurrentMonitorINA229 { // This structure is held in EEPROM, it has the same register/values // as the INA229 chip and is used to set the INA229 chip to the correct parameters on power up // On initial power up (or EEPROM clear) these parameters are read from the INA229 chip // to provide defaults. Some values are overridden in code (like ADC_CONFIG and CONFIG) // to configure to our prescribed needs. struct eeprom_regs { uint16_t R_CONFIG; uint16_t R_ADC_CONFIG; uint16_t R_SHUNT_CAL; // Shunt Calibration uint16_t R_SHUNT_TEMPCO; // Shunt Temperature Coefficient uint16_t R_DIAG_ALRT; uint16_t R_SOVL; uint16_t R_SUVL; uint16_t R_BOVL; uint16_t R_BUVL; uint16_t R_TEMP_LIMIT; uint16_t R_PWR_LIMIT;

    // LSB step size for the CURRENT register where the current in Amperes is stored
    float CURRENT_LSB;
    // Resistance of SHUNT in OHMS
    float RSHUNT;

    uint16_t shunt_max_current;
    uint16_t shunt_millivolt;
    uint16_t batterycapacity_amphour;
    float fully_charged_voltage;
    float tail_current_amps;
    float charge_efficiency_factor;
};

enum INA_REGISTER : uint8_t
{
    CONFIG = 0,
    ADC_CONFIG = 0,
    // Shunt Calibration
    SHUNT_CAL = 2,
    // Shunt Temperature Coefficient
    SHUNT_TEMPCO = 3,
    // Shunt Voltage Measurement 24bit
    VSHUNT = 4,
    // Bus Voltage Measurement 24bit
    VBUS = 5,
    DIETEMP = 6,
    // Current Result 24bit
    CURRENT = 7,
    // Power Result 24bit
    POWER = 8,
    // Energy Result 40bit
    ENERGY = 9,
    // Charge Result 40bit
    CHARGE = 0x0A,
    // Alert triggers
    DIAG_ALRT = 0x0b,
    // Shunt Overvoltage Threshold
    // overcurrent protection
    SOVL = 0x0c,
    // Shunt Undervoltage Threshold
    // undercurrent protection
    SUVL = 0x0d,
    // Bus Overvoltage Threshold
    BOVL = 0x0e,
    // Bus Undervoltage Threshold
    BUVL = 0x0f,
    // Temperature Over-Limit Threshold
    TEMP_LIMIT = 0x10,
    // Power Over-Limit Threshold
    PWR_LIMIT = 0x11,
    // Manufacturer ID
    MANUFACTURER_ID = 0x3E,
    // Device ID
    DEVICE_ID = 0x3F

};

enum DIAG_ALRT_FIELD : uint16_t
{
    ALATCH = 15,
    CNVR = 14,
    SLOWALERT = 13,
    APOL = 12,
    ENERGYOF = 11,
    CHARGEOF = 10,
    MATHOF = 9,
    RESERVED = 8,
    TMPOL = 7,
    SHNTOL = 6,
    SHNTUL = 5,
    BUSOL = 4,
    BUSUL = 3,
    POL = 2,
    CNVRF = 1,
    MEMSTAT = 0
};

const uint16_t ALL_ALERT_BITS = (bit(DIAG_ALRT_FIELD::TMPOL) |
                                 bit(DIAG_ALRT_FIELD::SHNTOL) |
                                 bit(DIAG_ALRT_FIELD::SHNTUL) |
                                 bit(DIAG_ALRT_FIELD::BUSOL) |
                                 bit(DIAG_ALRT_FIELD::BUSUL) |
                                 bit(DIAG_ALRT_FIELD::POL));

public: CurrentMonitorINA229() { INA229Installed = false; SPI_Ptr = NULL; chipselectpin = 0;

    milliamphour_out_lifetime = 0;
    milliamphour_in_lifetime = 0;

    daily_milliamphour_out = 0;
    daily_milliamphour_in = 0;

    milliamphour_out = 0;
    milliamphour_in = 0;

    milliamphour_out_offset = 0;
    milliamphour_in_offset = 0;

    // Clear structure
    memset(&registers, 0, sizeof(eeprom_regs));

    // Conversion times for voltage, current  temperature
    // 128 times sample averaging
    // MODE  = 1111 = Continuous bus, shunt voltage and temperature
    // VBUSCT= 111 = 6h = 4120µs BUS VOLT
    //  VSHCT= 111 = 6h = 4120µs CURRENT
    //   VTCT= 010 = 2h =  150µs TEMPERATURE
    //    AVG= 100 = 4h = 128 ADC sample averaging count
    // B1111 111 111 010 100 = 0xFFD4
    registers.R_ADC_CONFIG = 0xFFD4;

    registers.R_CONFIG = _BV(4); // ADCRANGE = 40.96mV scale

    // Defaults for battery capacity/voltages
    registers.batterycapacity_amphour = 280;
    registers.fully_charged_voltage = 3.50 * 16;
    registers.tail_current_amps = 20;
    registers.charge_efficiency_factor = 99.5;

    // Default 150A shunt @ 50mV scale
    registers.shunt_max_current = 50;
    registers.shunt_millivolt = 75;

    // SLOWALERT = Wait for full sample averaging time before triggering alert (about 1.5 seconds)
    registers.R_DIAG_ALRT = bit(DIAG_ALRT_FIELD::SLOWALERT);

    // This is not enabled by default
    // The 16 bit register provides a resolution of 1ppm/°C/LSB
    registers.R_SHUNT_TEMPCO = 15;

    // Use the defaults from the INA229 chip as a starting point
    registers.R_SOVL = 0x7FFF;
    registers.R_SUVL = 0x8000;
    // 85volt max
    registers.R_BOVL = 0x6A40;
    registers.R_BUVL = 0;
    registers.R_TEMP_LIMIT = 0x2800; // 80 degrees C

    CalculateLSB();

    // Default Power limit = 5kW
    registers.R_PWR_LIMIT = (uint16_t)((5000.0 / registers.CURRENT_LSB / 3.2) / 256.0); // 5kW
}

bool Available()
{
    return INA229Installed;
}

bool Initialise(SPIClass *SPI, uint8_t cs_pin);

bool Configure(uint16_t shuntmv,
               uint16_t shuntmaxcur,
               uint16_t batterycapacity,
               uint16_t fullchargevolt,
               uint16_t tailcurrent,
               uint16_t chargeefficiency,
               uint16_t shuntcal,
               int16_t temperaturelimit,
               int16_t overvoltagelimit,
               int16_t undervoltagelimit,
               int32_t overcurrentlimit,
               int32_t undercurrentlimit,
               int32_t overpowerlimit,
               uint16_t shunttempcoefficient,
               bool TemperatureCompEnabled);

void GuessSOC();
void TakeReadings();

uint32_t calc_milliamphour_out() { return milliamphour_out - milliamphour_out_offset; }
uint32_t calc_milliamphour_in() { return milliamphour_in - milliamphour_in_offset; }
uint32_t calc_daily_milliamphour_out() { return daily_milliamphour_out; }
uint32_t calc_daily_milliamphour_in() { return daily_milliamphour_in; }

float calc_charge_efficiency_factor() { return registers.charge_efficiency_factor; }
float calc_state_of_charge() { return SOC / 100.0; }

float calc_voltage() { return voltage; }
float calc_current() { return current; }
float calc_power() { return power; }
uint16_t calc_shuntcalibration() { return registers.R_SHUNT_CAL; }
int16_t calc_temperature() { return (int16_t)temperature; }
uint16_t calc_shunttempcoefficient() { return registers.R_SHUNT_TEMPCO; }
float calc_tailcurrentamps() { return registers.tail_current_amps; }
float calc_fullychargedvoltage() { return registers.fully_charged_voltage; }
float calc_shuntresistance() { return 1000 * registers.RSHUNT; }

uint16_t calc_shuntmillivolt() { return registers.shunt_millivolt; }
uint16_t calc_shuntmaxcurrent() { return registers.shunt_max_current; }
uint16_t calc_batterycapacityAh() { return registers.batterycapacity_amphour; }

int16_t calc_temperaturelimit()
{
    // Case unsigned to int16 to cope with negative temperatures
    return ConvertFrom2sComp(registers.R_TEMP_LIMIT) * (float)0.0078125;
}
float calc_overpowerlimit()
{
    return ConvertFrom2sComp(registers.R_PWR_LIMIT) * 256.0F * 3.2F * registers.CURRENT_LSB;
}
float calc_overvoltagelimit() { return (float)ConvertFrom2sComp(registers.R_BOVL) * 0.003125F; }
float calc_undervoltagelimit() { return (float)ConvertFrom2sComp(registers.R_BUVL) * 0.003125F; }
float calc_overcurrentlimit() { return ((float)ConvertFrom2sComp(registers.R_SOVL) / 1000 * 1.25) / full_scale_adc * registers.shunt_max_current; }
float calc_undercurrentlimit() { return ((float)ConvertFrom2sComp(registers.R_SUVL) / 1000 * 1.25) / full_scale_adc * registers.shunt_max_current; }

bool calc_tempcompenabled() { return (registers.R_CONFIG & bit(5)) != 0; }

uint16_t calc_alerts()
{
    registers.R_DIAG_ALRT = read16bits(INA_REGISTER::DIAG_ALRT);
    return registers.R_DIAG_ALRT & ALL_ALERT_BITS;
}
void SetSOC(uint16_t value);

void ResetDailyAmpHourCounters() {
    daily_milliamphour_out=0;
    daily_milliamphour_in=0;
}

private: uint16_t SOC = 0; float voltage = 0; float current = 0; float power = 0; float temperature = 0;

const float full_scale_adc = 312.5;
// const float CoulombsToAmpHours = 1.0 / 3600.0;
const float CoulombsToMilliAmpHours = 1.0 / 3.6;

uint8_t max_soc_reset_counter = 0;
int64_t soc_reset_time;
int32_t last_charge_coulombs = 0;

// Pointer to SPI object class  NOTE: MUTEX OVER SPI PORT MUST BE HANDLED EXTERNALLY TO THIS CLASS
SPIClass *SPI_Ptr;
// True is chip is installed
bool INA229Installed;
// Chip select pin
uint8_t chipselectpin;
// Settings used for SPI comms (10Mhz, MSB, Mode 1)
SPISettings _spisettings = SPISettings(10000000, MSBFIRST, SPI_MODE1);

eeprom_regs registers;

uint32_t milliamphour_out_lifetime;
uint32_t milliamphour_in_lifetime;

uint32_t daily_milliamphour_out;
uint32_t daily_milliamphour_in;

uint32_t milliamphour_out;
uint32_t milliamphour_in;

uint32_t milliamphour_out_offset;
uint32_t milliamphour_in_offset;

volatile uint16_t diag_alrt_value = 0;

uint8_t readRegisterValue(INA_REGISTER r);
uint8_t writeRegisterValue(INA_REGISTER r);
void CalculateLSB();

uint16_t read16bits(INA_REGISTER r);
uint16_t write16bits(INA_REGISTER r, uint16_t value);
void SetINA229Registers();
float BusVoltage();
float Energy();
float ShuntVoltage();

int32_t ChargeInCoulombsAsInt();

int32_t readInt20(INA_REGISTER r);
uint32_t readUInt24(INA_REGISTER r);
uint32_t spi_readUint24(INA_REGISTER r);
uint64_t spi_readUint40(INA_REGISTER r);
int64_t spi_readInt40(INA_REGISTER r);

void CalculateAmpHourCounts();
uint16_t CalculateSOC();

// Convert an int16 to a uint16 2 compliment value
uint16_t ConvertTo2sComp(int16_t value)
{
    if (value >= 0)
    {
        return (uint16_t)value;
    }

    uint16_t v = -value;
    ESP_LOGI(TAG, "uint16 v=%u", v);

    v = v - 1;
    v = ~v;
    v = v | 0x8000U;
    return v;
}

// Convert a 2 compliemnt uint16 to a signed int16 value
int16_t ConvertFrom2sComp(uint16_t value)
{
    int16_t v = (int16_t)value;

    if (value & 0x8000U == 0)
    {
        // Positive number, just return it
        return v;
    }

    // Invert
    value = ~value;
    // Add 1
    value = value + 1;
    // Return NEGATIVE of the value
    v = -value;
    return v;
}

// Calculated power output.  Output value in watts. Unsigned representation. Positive value.
float Power()
{
    // POWER Power [W] = 3.2 x CURRENT_LSB x POWER
    return (float)spi_readUint24(INA_REGISTER::POWER) * (float)3.2 * registers.CURRENT_LSB;
}

// The INA228 device has an internal temperature sensor which can measure die temperature from –40 °C to +125°C.
float DieTemperature()
{
    // The accuracy of the temperature sensor is ±2 °C across the operational temperature range. The temperature
    // value is stored inside the DIETEMP register
    // Internal die temperature measurement.
    // Case unsigned to int16 to cope with negative temperatures
    // Two's complement value. Conversion factor: 7.8125 m°C/LSB
    float dietemp = ConvertFrom2sComp(read16bits(INA_REGISTER::DIETEMP));
    return dietemp * (float)0.0078125;
}

float TemperatureLimit()
{
    // Case unsigned to int16 to cope with negative temperatures
    float temp = ConvertFrom2sComp(read16bits(INA_REGISTER::TEMP_LIMIT));
    return temp * (float)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
float Current()
{
    // Current. Two's complement value.
    return -(registers.CURRENT_LSB * (float)readInt20(INA_REGISTER::CURRENT));
}

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
    write16bits(INA_REGISTER::CONFIG, registers.R_CONFIG | (uint16_t)_BV(14));
}

void SetINA229ConfigurationRegisters()
{
    write16bits(INA_REGISTER::CONFIG, registers.R_CONFIG);
    write16bits(INA_REGISTER::ADC_CONFIG, registers.R_ADC_CONFIG);
}

};

endif

` Capture

delboy711 commented 1 year ago

I have run into the same issue. My 50A 75mV shunt did not read any current higher than 27A. I finally realised the issue was the 40.97mV range set on the INA229 and not a problem with my inverter.

Looking at your code it is hard to work out what changes you have made, but you do not seem to have changed the ADCRANGE flag. This is set in the function Configure()

From line ~489

        //registers.R_CONFIG = _BV(4); // ADCRANGE = 40.96mV scale
        registers.R_CONFIG &= 0xffef ; //Set  ADCRANGE  to  163.84 mV scale
delboy711 commented 1 year ago

I have it working now for my 50A 75mV shunt. The attached patch should work for any type of shunt. For 50mV shunts it sets the ADCRANGE to 40.96mV and for higher Voltage shunts it sets the range to 163.84mV.

The only change in the calculations is R_SHUNT_CAL is 4 times larger when the 40.96mV range is selected.file:///home/derek/Desktop/INA229.diff

diff --git a/ESPController/include/CurrentMonitorINA229.h b/ESPController/include/CurrentMonitorINA229.h
index 599f2ef..0557c01 100644
--- a/ESPController/include/CurrentMonitorINA229.h
+++ b/ESPController/include/CurrentMonitorINA229.h
@@ -293,7 +293,7 @@ private:
     float power = 0;
     float temperature = 0;

-    const float full_scale_adc = 40.96;
+    float full_scale_adc = 40.96;
     // const float CoulombsToAmpHours = 1.0 / 3600.0;
     const float CoulombsToMilliAmpHours = 1.0 / 3.6;

diff --git a/ESPController/src/CurrentMonitorINA229.cpp b/ESPController/src/CurrentMonitorINA229.cpp
index 32c1f9e..0ef51d5 100644
--- a/ESPController/src/CurrentMonitorINA229.cpp
+++ b/ESPController/src/CurrentMonitorINA229.cpp
@@ -36,32 +36,39 @@ void CurrentMonitorINA229::CalculateLSB()
     // in above - click COG icon top left.
     // https://e2e.ti.com/support/amplifiers-group/amplifiers/f/amplifiers-forum/1034569/ina228-accumulated-energy-and-charge-is-wrong

-    // 150A/50mV shunt =   full_scale_current= 150.00A / 50.00 * 40.96 = 122.88 AMPS
+    // 150A/50mV shunt =   full_scale_current= 150.00A / 50.00 * 40.96 = 122.88 AMPS ADCRANGE=1
     //                     RSHUNT = (50 / 1000) / 150 = 0.00033333333
     //                     CURRENT_LSB = 150/ 524288 = 0.000286102294921875
     //                     R_SHUNT_CAL = 52428800000*0.000286102294921875*0.00033333333 = 4999.999 = 5000

-    // 300A/75mV shunt =   full_scale_current= 300.00A / 75.00 * 40.96 = 163.84 AMPS
-    //                     RSHUNT = (75 / 1000) / 163.84 = 0.000457763671875
-    //                     CURRENT_LSB = 163.84/ 524288 = 2.648162841796875e-4
-    //                     R_SHUNT_CAL = 52428800000*0.00057220458984375*0.00025 = 7499.9922688 = 7500
+    // 300A/75mV shunt =   full_scale_current= 300.00A  ADCRANGE=0
+    //                     RSHUNT = (75 / 1000) / 300 = 0.00025
+    //                     CURRENT_LSB = 300/ 524288 = 5.72204589844e-4 = 0.000572204589844
+    //                     R_SHUNT_CAL = 13107.2e6 * 5.72204589844e-4 * 0.00025 = 1875
+
+    // 50A/75mV shunt =   full_scale_current=50.00A  ADCRANGE=0
+    //                     RSHUNT = (75 / 1000) / 50 = 0.0015
+    //                     CURRENT_LSB = 50/ 524288 = 9.53674316406e-05 = 0.0000953674316406
+    //                     R_SHUNT_CAL = 13107.2e6 * 9.53674316406e-05 * 0.0015 =    1875

     // Calculate CURRENT_LSB and R_SHUNT_CAL values
     // registers.full_scale_current = ((float)registers.shunt_max_current / (float)registers.shunt_millivolt) * full_scale_adc;
     registers.RSHUNT = ((float)registers.shunt_millivolt / 1000.0) / (float)registers.shunt_max_current;
     registers.CURRENT_LSB = registers.shunt_max_current / (float)0x80000;
-    registers.R_SHUNT_CAL = 4L * (13107200000L * registers.CURRENT_LSB * registers.RSHUNT);
+    registers.R_SHUNT_CAL = (13107200000L * registers.CURRENT_LSB * registers.RSHUNT);
+    if(registers.shunt_millivolt <= 50) registers.R_SHUNT_CAL * 4L;

     ESP_LOGI(TAG, "RSHUNT=%.8f, CURRENT_LSB=%.8f, R_SHUNT_CAL=%u", registers.RSHUNT, registers.CURRENT_LSB, registers.R_SHUNT_CAL);

-    // Will the full scale current be over the 40.96mV range the ADC can handle?
+    // Will the full scale current be over the range the ADC can handle?
     if (((float)registers.shunt_max_current * registers.RSHUNT * 1000.0) > full_scale_adc)
     {
         float true_max_current = ((float)registers.shunt_max_current / (float)registers.shunt_millivolt) * full_scale_adc;
         ESP_LOGW(TAG, "WARNING: True Max Current Measurable %.2f Amps", true_max_current);

         registers.CURRENT_LSB = true_max_current / (float)0x80000;
-        registers.R_SHUNT_CAL = 4L * (13107200000L * registers.CURRENT_LSB * registers.RSHUNT);
+        registers.R_SHUNT_CAL = (13107200000L * registers.CURRENT_LSB * registers.RSHUNT);
+        if(registers.shunt_millivolt <= 50) registers.R_SHUNT_CAL * 4L;
         ESP_LOGI(TAG, "RECALC: RSHUNT=%.8f, CURRENT_LSB=%.8f, R_SHUNT_CAL=%u", registers.RSHUNT, registers.CURRENT_LSB, registers.R_SHUNT_CAL);
     }

@@ -456,6 +463,8 @@ bool CurrentMonitorINA229::Configure(uint16_t shuntmv,
     registers.tail_current_amps = tailcurrent / 100.0;
     registers.charge_efficiency_factor = chargeefficiency / 100.0;

+    full_scale_adc = (registers.shunt_millivolt > 50) ? 163.84 : 40.96;     //Select ADC Range
+
     CalculateLSB();

     if (shuntcal != 0 && registers.R_SHUNT_CAL != shuntcal)
@@ -486,7 +495,7 @@ bool CurrentMonitorINA229::Configure(uint16_t shuntmv,

     // ESP_LOGI(TAG, "undercurrentlimit=%f, R_SUVL=%u", ((float)undercurrentlimit / 100.0F), registers.R_SUVL);

-    registers.R_CONFIG = _BV(4); // ADCRANGE = 40.96mV scale
+    registers.R_CONFIG =(registers.shunt_millivolt > 50) ? registers.R_CONFIG & 0xffef: _BV(4);  // ADCRANGE = 163.84mV or 40.96mV scale

     if (TemperatureCompEnabled)
     {
diff --git a/ESPController/web_src/default.htm b/ESPController/web_src/default.htm
index de58b13..b31e2aa 100644
--- a/ESPController/web_src/default.htm
+++ b/ESPController/web_src/default.htm
@@ -1395,7 +1395,7 @@
         <h2>Basic Settings</h2>
         <p id="b3">Ensure the current shunt parameters match the data sheet for your particular shunt resistor.</p>
         <p id="b4">
-          The DIYBMS current monitor uses a 40.96mV maximum scale, so shunt voltages over this will be scaled down
+          For 50mA internal shunts DIYBMS current monitor uses a 40.96mV maximum scale, so shunt voltages over this will be scaled down
           proportionally.
         </p>
         <p id="b11">
lorenzo6201 commented 1 year ago

can you post the entire file in a zip. I tried this code , but I think i did something wrong , because it does not work. I`m not that good with programing.

delboy711 commented 1 year ago

Sure. I have applied it to my fork of Stuart's repository at https://github.com/delboy711/diyBMSv4ESP32 You can download a Zip file of the entire code there and compile it in the same way as you do Stuart's. Are you OK with compiling in PlatformIO?

The affected files are ESPController/include/CurrentMonitorINA229.h ESPController/src/CurrentMonitorINA229.cpp ESPController/web_src/default.htm (just trivial changes to the text. )

lorenzo6201 commented 1 year ago

i`m okay with compiling. Thank you