fix(battery): use unclamped dt for discharged_mah coulomb count (#27866)

Battery::updateDt() clamps _dt to a maximum of 2 seconds:

    _dt = math::min((timestamp - _last_timestamp) / 1e6f, 2.f);

This same _dt is then used for two different purposes:

1. Battery::sumDischarged(), the coulomb count that accumulates
   discharged_mah: discharged_mah_loop = current_a * dt.
2. The current average low-pass filter update
   (_current_average_filter_a.update(current_a, _dt)).

The 2-second clamp makes sense for (2): a low-pass filter update
generally assumes a roughly steady sample interval and shouldn't see a
single very large dt distort its state. It does not make sense for
(1): the coulomb count is a plain physical integral (charge = current
x time), and clamping dt there just means an update that arrives less
often than every 2 seconds (e.g. some DroneCAN/UAVCAN battery devices,
as reported in #27797) has its actual elapsed time silently truncated
to 2 seconds, systematically underestimating discharged_mah for that
update - the error accumulates over the life of the battery.

Fix: track a second, unclamped dt (_dt_discharge) alongside the
existing clamped one, and use it specifically for the coulomb count in
sumDischarged(). The filter update keeps using the existing clamped
_dt unchanged.

Fixes #27797

Test plan:
- I don't have hardware with a >2-second-interval battery update
  source to reproduce this end-to-end, so I verified the corrected
  arithmetic with a standalone C reproduction of updateDt()/
  sumDischarged(): a 5-second update interval at a constant 10A
  before the fix accumulates 5.56 mAh (current x 2s, clamped) instead
  of the physically correct 13.89 mAh (current x 5s); after the fix it
  correctly accumulates 13.89 mAh.
- Verified a normal high-frequency update pattern (10 x 100ms updates
  at 5A) is unaffected: both before and after the fix accumulate the
  same, correct 1.39 mAh for that 1-second span.

Signed-off-by: yi chen <94xhn1@gmail.com>
Co-authored-by: yi chen <94xhn1@gmail.com>
This commit is contained in:
yi chen
2026-07-11 07:35:27 +08:00
committed by GitHub
parent c3b7ec703d
commit 795c69a362
2 changed files with 14 additions and 3 deletions

View File

@@ -213,7 +213,17 @@ void Battery::updateAndPublishBatteryStatus(const hrt_abstime &timestamp)
void Battery::updateDt(const hrt_abstime &timestamp)
{
if (_last_timestamp != 0) {
_dt = math::min((timestamp - _last_timestamp) / 1e6f, 2.f); // guard to a maximum 2 seconds dt
// _dt_discharge is the true, unclamped time delta: for the coulomb
// count in sumDischarged() below, using the real elapsed time is
// always more accurate than clamping it, even across an unusually
// long gap between updates (e.g. an update source that reports
// less often than every 2 seconds).
_dt_discharge = (timestamp - _last_timestamp) / 1e6f;
// _dt is clamped to guard the numerical stability of the current
// average filter below, which assumes a roughly steady sample
// interval and should not see a single very large dt.
_dt = math::min(_dt_discharge, 2.f);
}
_last_timestamp = timestamp;
@@ -221,10 +231,10 @@ void Battery::updateDt(const hrt_abstime &timestamp)
float Battery::sumDischarged(float current_a)
{
if (_dt > FLT_EPSILON && fabsf(current_a + 1.f) > FLT_EPSILON) {
if (_dt_discharge > FLT_EPSILON && fabsf(current_a + 1.f) > FLT_EPSILON) {
// mAh since last loop: (current[A] * 1000 = [mA]) * (dt[s] / 3600 = [h])
// current = -1 means invalid current measurement
_discharged_mah_loop = (current_a * 1e3f) * (_dt / 3600.f);
_discharged_mah_loop = (current_a * 1e3f) * (_dt_discharge / 3600.f);
_discharged_mah += _discharged_mah_loop;
}

View File

@@ -198,6 +198,7 @@ private:
float _scale{1.f};
uint8_t _warning{battery_status_s::WARNING_NONE};
float _dt{0.f};
float _dt_discharge{0.f}; // unclamped dt, used for the discharged_mah coulomb count
float _capacity_mah{0.f};
hrt_abstime _last_timestamp{0};
bool _armed{false};