forked from tbnobody/OpenDTU
-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
BatteryGuard: Open circuit voltage and internal resistance
- Loading branch information
Showing
9 changed files
with
367 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
#pragma once | ||
#include <Arduino.h> | ||
#include <frozen/string.h> | ||
#include <TaskSchedulerDeclarations.h> | ||
#include "Statistic.h" | ||
|
||
|
||
class BatteryGuardClass { | ||
public: | ||
BatteryGuardClass() = default; | ||
~BatteryGuardClass() = default; | ||
|
||
void init(Scheduler& scheduler); | ||
void updateSettings(void); | ||
|
||
std::optional<float> calculateOpenCircuitVoltage(float const nowVoltage, float const nowCurrent); | ||
std::optional<float> getOpenCircuitVoltage(void) const; | ||
std::optional<float> calculateInternalResistance(float const nowVoltage, float const nowCurrent); | ||
std::optional<float> getInternalResistance(void) const; | ||
|
||
private: | ||
enum class Text : uint8_t { | ||
Q_NODATA = 0, | ||
Q_EXCELLENT = 1, | ||
Q_GOOD = 2, | ||
Q_BAD = 3, | ||
T_HEAD = 4 | ||
}; | ||
void loop(void); | ||
void printOpenCircuitVoltageInformationBlock(void); | ||
frozen::string const& getText(Text tNr); | ||
|
||
// used for calculation of the "Open circuit voltage" | ||
WeightedAVG<float> _openCircuitVoltageAVG {10}; // battery open circuit voltage (average factor 10%) | ||
uint32_t _lastOCVMillis = 0; // last millis of calculation of the open circuit voltage | ||
float _resistorConfig = 0.0f; // value from configuration or resistance calculation | ||
|
||
// used for calculation of the "Battery internal resistance" | ||
WeightedAVG<float> _internalResistanceAVG {10}; // resistor (average factor 10%) | ||
bool _firstOfTwoAvailable = false; // false after to got the first of two values | ||
bool _minMaxAvailable = false; // minimum and maximum values available | ||
std::pair<float,float> _firstVolt = {0.0f,0.0f}; // first of two voltage and related current | ||
std::pair<float,float> _maxVolt = {0.0f,0.0f}; // maximum voltage and related current | ||
std::pair<float,float> _minVolt = {0.0f,0.0f}; // minimum voltage and related current | ||
uint32_t _lastMinMaxMillis = 0; // last millis from the first min/max values | ||
float const _minDiffVoltage = 0.05f; // 50mV minimum difference to calculate a resistance (Smart Shunt) | ||
// unclear if this value will also fit to other battery provider | ||
|
||
Task _loopTask; // Task | ||
bool _verboseLogging = false; // Logging On/Off | ||
bool _useBatteryGuard = false; // "Battery guard" On/Off | ||
}; | ||
|
||
extern BatteryGuardClass BatteryGuard; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
#pragma once | ||
|
||
/* | ||
* Weighted average and statistics class (initialising value defines the weighted average 10 = 10%) | ||
*/ | ||
template <typename T> | ||
class WeightedAVG { | ||
public: | ||
explicit WeightedAVG(size_t factor) | ||
: _countMax(factor) | ||
, _count(0), _countNum(0), _avgV(0), _minV(0), _maxV(0), _lastV(0) {} | ||
|
||
// Add a value to the statistics | ||
void addNumber(const T& num) { | ||
if (_count == 0){ | ||
_count++; | ||
_avgV = num; | ||
_minV = num; | ||
_maxV = num; | ||
_countNum = 1; | ||
} else { | ||
if (_count < _countMax) | ||
_count++; | ||
_avgV = (_avgV * (_count - 1) + num) / _count; | ||
if (num < _minV) { _minV = num; } | ||
if (num > _maxV) { _maxV = num; } | ||
if (_countNum < 10000) { _countNum++; } | ||
} | ||
_lastV = num; | ||
} | ||
|
||
// Reset the statistic data | ||
void reset(void) { _count = 0; _avgV = 0; _minV = 0; _maxV = 0; _lastV = 0; _countNum = 0; } | ||
// Reset the statistic data and initialize with first value | ||
void reset(const T& num) { _count = 0; addNumber(num); } | ||
// Returns the weighted average | ||
T getAverage() const { return _avgV; } | ||
// Returns the minimum value | ||
T getMin() const { return _minV; } | ||
// Returns the maximum value | ||
T getMax() const { return _maxV; } | ||
// Returns the last added value | ||
T getLast() const { return _lastV; } | ||
// Returns the amount of added values. Limited to 10000 | ||
size_t getCounts() const { return _countNum; } | ||
|
||
private: | ||
size_t _countMax; // weighting factor (10 => 1/10 => 10%) | ||
size_t _count; // counter (0 - _countMax) | ||
size_t _countNum; // counts the amount of added values (0 - 10000) | ||
T _avgV; // average value | ||
T _minV; // minimum value | ||
T _maxV; // maximum value | ||
T _lastV; // last value | ||
}; | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
/* Battery-Guard | ||
* | ||
* The Battery-Guard has several functions. | ||
* - Calculate the battery internal resistance | ||
* - Calculate the battery open circuit voltage | ||
* - Limit the power drawn from the battery, if the battery voltage is close to the stop threshold. (draft) | ||
* - Periodically recharge the battery to 100% SoC (draft) | ||
* | ||
* Basic principe of the function: "Battery internal resistance" | ||
* Collects minimum and maximum values (voltage and current) over a time frame. Calculates the resistance from these values | ||
* and build a weighed average. | ||
* | ||
* Basic principe of the function: "Open circuit voltage" | ||
* Use the battery internal resistance to calculate the open circuit voltage and build a weighed average. | ||
* | ||
* Basic principe of the function: "Low voltage limiter" | ||
* If the battery voltage is close to the stop threshold, the battery limiter will calculate a maximum power limit | ||
* to keep the battery voltage above the voltage threshold. | ||
* The inverter is only switched-off when the threshold is exceeded and the inverter output cannot be reduced any further. | ||
* | ||
* Basic principe of the function: "Periodically recharge the battery" | ||
* After some days we start to reduce barriers, to make it more easier to fully charge the battery. | ||
* When we reach 100% SoC we remove all restrictions and start a new period. | ||
* Especially usefull during winter to calibrate the SoC calculation of the BMS | ||
* | ||
* Notes: | ||
* Some function are still under development. | ||
* | ||
* 01.08.2024 - 0.1 - first version. "Low voltage power limiter" | ||
* 09.12.2024 - 0.2 - add of function "Periodically recharge the battery" | ||
* 11.12.2024 - 0.3 - add of function "Battery internal resistance" and "Open circuit voltage" | ||
*/ | ||
|
||
#include <frozen/map.h> | ||
#include "Configuration.h" | ||
#include "MessageOutput.h" | ||
#include "BatteryGuard.h" | ||
|
||
|
||
// support for debugging, 0 = without extended logging, 1 = with extended logging, 2 = with much more logging | ||
constexpr int MODULE_DEBUG = 0; | ||
|
||
BatteryGuardClass BatteryGuard; | ||
|
||
|
||
/* | ||
* Initialize the battery guard | ||
*/ | ||
void BatteryGuardClass::init(Scheduler& scheduler) { | ||
|
||
// init the task loop | ||
scheduler.addTask(_loopTask); | ||
_loopTask.setCallback(std::bind(&BatteryGuardClass::loop, this)); | ||
_loopTask.setIterations(TASK_FOREVER); | ||
_loopTask.setInterval(60*1000); | ||
_loopTask.enable(); | ||
|
||
updateSettings(); | ||
} | ||
|
||
|
||
/* | ||
* Update some settings of the battery guard | ||
*/ | ||
void BatteryGuardClass::updateSettings(void) { | ||
|
||
// todo: get values from the configuration | ||
_verboseLogging = true; | ||
_useBatteryGuard = true; | ||
|
||
// used for "Open circuit voltage" | ||
_resistorConfig = 0.012f; | ||
} | ||
|
||
|
||
/* | ||
* Periodical tasks, will be called once a minute | ||
*/ | ||
void BatteryGuardClass::loop(void) { | ||
|
||
if (_useBatteryGuard && _verboseLogging) { | ||
MessageOutput.printf("%s\r\n", getText(Text::T_HEAD).data()); | ||
MessageOutput.printf("%s ---------------- Battery-Guard information block (every minute) ----------------\r\n", | ||
getText(Text::T_HEAD).data()); | ||
MessageOutput.printf("%s\r\n", getText(Text::T_HEAD).data()); | ||
} | ||
|
||
// "Open circuit voltage" | ||
if (_useBatteryGuard && _verboseLogging) { | ||
printOpenCircuitVoltageInformationBlock(); | ||
} | ||
|
||
// "Low voltage power limiter" | ||
|
||
|
||
// "Periodically recharge the battery" | ||
|
||
} | ||
|
||
|
||
|
||
/* | ||
* Calculate the battery open circuit voltage. | ||
* Returns the weighted average value or nullptr if calculation is not possible or if the value is out of date. | ||
*/ | ||
std::optional<float> BatteryGuardClass::calculateOpenCircuitVoltage(float const nowVoltage, float const nowCurrent) { | ||
|
||
// calculate the open circuit battery voltage (current flow into the battery must be positive) | ||
auto oResistor = getInternalResistance(); | ||
if ((nowVoltage > 0.0f) && (oResistor.has_value())) { | ||
_openCircuitVoltageAVG.addNumber(nowVoltage - nowCurrent * oResistor.value()); | ||
_lastOCVMillis = millis(); | ||
} | ||
return getOpenCircuitVoltage(); | ||
} | ||
|
||
|
||
/* | ||
* Returns the battery internal resistance, calculated / configured or nullopt if neither value is valid | ||
*/ | ||
std::optional<float> BatteryGuardClass::getInternalResistance(void) const { | ||
if (_internalResistanceAVG.getCounts() > 4) { return _internalResistanceAVG.getAverage(); } | ||
if (_resistorConfig != 0.0f) { return _resistorConfig; } | ||
return std::nullopt; | ||
} | ||
|
||
|
||
/* | ||
* Returns the battery open circuit voltage or nullopt if value is not valid | ||
*/ | ||
std::optional<float> BatteryGuardClass::getOpenCircuitVoltage(void) const { | ||
if ((_openCircuitVoltageAVG.getCounts() > 0) && (millis() - _lastOCVMillis) < 30*1000) { | ||
return _openCircuitVoltageAVG.getAverage(); | ||
} else { | ||
return std::nullopt; | ||
} | ||
} | ||
|
||
|
||
/* | ||
* Calculate the battery internal resistance between the battery cells and the voltage measurement device. (BMS, MPPT, Inverter) | ||
* Returns the resistance, calculated / configured or nullopt if neither value is valid | ||
*/ | ||
std::optional<float> BatteryGuardClass::calculateInternalResistance(float const nowVoltage, float const nowCurrent) { | ||
|
||
if (nowVoltage <= 0.0f) { return getInternalResistance(); } | ||
|
||
// we must avoid to use measurement values during any power transition. | ||
// To solve this problem, we check whether two consecutive measurements are almost identical (5mV, 200mA) | ||
if (!_firstOfTwoAvailable || (std::abs(_firstVolt.first - nowVoltage) > 0.005f) || | ||
(std::abs(_firstVolt.second - nowCurrent) > 0.2f)) { | ||
_firstVolt.first = nowVoltage; | ||
_firstVolt.second = nowCurrent; | ||
_firstOfTwoAvailable = true; | ||
return getInternalResistance(); | ||
} | ||
_firstOfTwoAvailable = false; // prepair for the next calculation | ||
|
||
// store the average in min or max buffer | ||
std::pair<float,float> avgVolt = std::make_pair((nowVoltage + _firstVolt.first) / 2.0f, (nowCurrent + _firstVolt.second) / 2.0f); | ||
if (!_minMaxAvailable) { | ||
_minVolt = _maxVolt = avgVolt; | ||
_lastMinMaxMillis = millis(); | ||
_minMaxAvailable = true; | ||
} else { | ||
if (avgVolt.first < _minVolt.first) { _minVolt = avgVolt; } | ||
if (avgVolt.first > _maxVolt.first) { _maxVolt = avgVolt; } | ||
} | ||
|
||
// we evaluate min and max values in a time duration of 30 sec | ||
if ((!_minMaxAvailable || (millis() - _lastMinMaxMillis) < 30*1000)) { return getInternalResistance(); } | ||
_minMaxAvailable = false; // prepair for the next calculation | ||
|
||
// we need a minimum voltage difference to get a sufficiently good result (failure < 10%) | ||
// SmartShunt: 50mV (about 100W on VDC: 24V, Ri: 12mOhm) | ||
if ((_maxVolt.first - _minVolt.first) >= _minDiffVoltage) { | ||
float resistor = std::abs((_maxVolt.first - _minVolt.first) / (_maxVolt.second - _minVolt.second)); | ||
|
||
// we try to keep out bad values from the average | ||
if (_internalResistanceAVG.getCounts() < 10) { | ||
_internalResistanceAVG.addNumber(resistor); | ||
} else { | ||
if ((resistor > _internalResistanceAVG.getAverage() / 2.0f) && (resistor < _internalResistanceAVG.getAverage() * 2.0f)) { | ||
_internalResistanceAVG.addNumber(resistor); | ||
} | ||
} | ||
|
||
// todo: delete after testing | ||
if constexpr(MODULE_DEBUG >= 1) { | ||
MessageOutput.printf("%s Resistor - Calculated: %0.3fOhm\r\n", getText(Text::T_HEAD).data(), resistor); | ||
} | ||
} | ||
return getInternalResistance(); | ||
} | ||
|
||
|
||
/* | ||
* prints the "Battery open circuit voltage" information block | ||
*/ | ||
void BatteryGuardClass::printOpenCircuitVoltageInformationBlock(void) | ||
{ | ||
MessageOutput.printf("%s 1) Function: Battery open circuit voltage\r\n", | ||
getText(Text::T_HEAD).data()); | ||
|
||
MessageOutput.printf("%s Open circuit voltage: %0.3fV\r\n", | ||
getText(Text::T_HEAD).data(), _openCircuitVoltageAVG.getAverage()); | ||
|
||
MessageOutput.printf("%s Internal resistance: %0.4fOhm (Min: %0.4f, Max: %0.4f, Last: %0.4f, Amount: %i)\r\n", | ||
getText(Text::T_HEAD).data(), _internalResistanceAVG.getAverage(), _internalResistanceAVG.getMin(), | ||
_internalResistanceAVG.getMax(), _internalResistanceAVG.getLast(), _internalResistanceAVG.getCounts() - 1); | ||
} | ||
|
||
|
||
/* | ||
* Returns a string according to current text nr | ||
*/ | ||
frozen::string const& BatteryGuardClass::getText(BatteryGuardClass::Text tNr) | ||
{ | ||
static const frozen::string missing = "programmer error: missing status text"; | ||
|
||
static const frozen::map<Text, frozen::string, 5> texts = { | ||
{ Text::Q_NODATA, "Insufficient data" }, | ||
{ Text::Q_EXCELLENT, "Excellent" }, | ||
{ Text::Q_GOOD, "Good" }, | ||
{ Text::Q_BAD, "Bad" }, | ||
{ Text::T_HEAD, "[Battery-Guard]"} | ||
}; | ||
|
||
auto iter = texts.find(tNr); | ||
if (iter == texts.end()) { return missing; } | ||
|
||
return iter->second; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.