Status of battery sag compensation

I’m about to switch from LiPo to Li-Ion on one of my copters.

The issue of greater battery sag of Li-Ion was brought to my attention - so I’ve been trying to dig into that and see if I need to set any parameters to cope with the issue.

According to this message thread, Randy Mackay notes that the firmware sets the parameter values necessary to compensate for this automatically.

As it’s been some time since this message thread, I was wondering if someone might know if this is still essentially the same today - or if there are parameters that need to be use by the user to appropriately deal with Li-Ion battery sag.

Thank you!

Sag calculation appears to be fully automated. Set BATT_FS_VOLTSRC to 1 if you want your failsafes to use the compensated value (which should allow for longer flight times).

1 Like

Thanks for looking into this Yuri - I appreciate it.

I would be interesting to know more about how this capability operates. As I understood the posting I referenced above by @rmackay9 - the firmware is always automatically always evaluating sag. I can imagine there are various degrees of sag from any number of causes besides different battery chemistry.

Is it possible to know who wrote this section of code? Perhaps there’s a way to direct a question to that person - to save you the trouble, and avoid any confusion.

I’m wondering if this capability was designed to prevent inadvertent battery-failsafe occurrences during intermittent loads caused by flight demands (climbs, winds, etc) or if it addresses the needs for better battery failsafe behavior near battery depletion.

If the latter is the case - proper analysis of typical voltages over test missions ought to make it possible to set proper battery failsafe voltage levels without any optional firmware feature assistance.

That makes me curios about the intended benefit of setting BATT_FS_VOLTSRC=1.

If the former is the case - that the capability protects against inadvertent sag due to flight operations - then using this capability wouldn’t extend flight time - only prevent inadvertent premature failsafes.

Is it possible to put me in touch with the people who were involved with this capability to fully understand it’s usefulness?

Thank you!

Just read the source yourself. It’s all there and reads pretty cleanly.

I’m sorry Yuri - even if my coding skills weren’t rusty and ancient, I wouldn’t trust myself to understand the relevance of it due to possible interdependencies. Since it’s function isn’t documented, I have to rely on those who’s skills are up to the task.

If this is something you’re unable to help me out with, I’ll simply repost the request for help and hope someone more familiar with this capability will respond. Thanks!

Why would you repost the same question? That’s nonsense. I highly recommend that you make an effort to dig in and read some of the source, learn a bit about git, and then help contribute the documentation you so frequently find lacking.

Some more info for you, from libraries/AP_BattMonitor/AP_BattMonitor_Backend.cpp:

The following function is called at a pretty high frequency, attempting to estimate resistance based on changes in measured current and voltage, which allows for estimated resting voltage.

void AP_BattMonitor_Backend::update_resistance_estimate()
    // return immediately if no current
    if (!has_current() || !is_positive(_state.current_amps)) {

    // update maximum current seen since startup and protect against divide by zero
    _current_max_amps = MAX(_current_max_amps, _state.current_amps);
    float current_delta = _state.current_amps - _current_filt_amps;
    if (is_zero(current_delta)) {

    // update reference voltage and current
    if (_state.voltage > _resistance_voltage_ref) {
        _resistance_voltage_ref = _state.voltage;
        _resistance_current_ref = _state.current_amps;

    // calculate time since last update
    uint32_t now = AP_HAL::millis();
    float loop_interval = (now - _resistance_timer_ms) * 0.001f;
    _resistance_timer_ms = now;

    // estimate short-term resistance
    float filt_alpha = constrain_float(loop_interval/(loop_interval + AP_BATT_MONITOR_RES_EST_TC_1), 0.0f, 0.5f);
    float resistance_alpha = MIN(1, AP_BATT_MONITOR_RES_EST_TC_2*fabsf((_state.current_amps-_current_filt_amps)/_current_max_amps));
    float resistance_estimate = -(_state.voltage-_voltage_filt)/current_delta;
    if (is_positive(resistance_estimate)) {
        _state.resistance = _state.resistance*(1-resistance_alpha) + resistance_estimate*resistance_alpha;

    // calculate maximum resistance
    if ((_resistance_voltage_ref > _state.voltage) && (_state.current_amps > _resistance_current_ref)) {
        float resistance_max = (_resistance_voltage_ref - _state.voltage) / (_state.current_amps - _resistance_current_ref);
        _state.resistance = MIN(_state.resistance, resistance_max);

    // update the filtered voltage and currents
    _voltage_filt = _voltage_filt*(1-filt_alpha) + _state.voltage*filt_alpha;
    _current_filt_amps = _current_filt_amps*(1-filt_alpha) + _state.current_amps*filt_alpha;

    // update estimated voltage without sag
    _state.voltage_resting_estimate = _state.voltage + _state.current_amps * _state.resistance;

If BATT_FS_VOLTSRC is set to 1, then the estimated resting state is used for failsafe triggers rather than the instantaneous measurement, as seen here:

void AP_BattMonitor_Backend::check_failsafe_types(bool &low_voltage, bool &low_capacity, bool &critical_voltage, bool &critical_capacity) const
    // use voltage or sag compensated voltage
    float voltage_used;
    switch (_params.failsafe_voltage_source()) {
        case AP_BattMonitor_Params::BattMonitor_LowVoltageSource_Raw:
            voltage_used = _state.voltage;
        case AP_BattMonitor_Params::BattMonitor_LowVoltageSource_SagCompensated:
            voltage_used = voltage_resting_estimate();

    // check critical battery levels
    if ((voltage_used > 0) && (_params._critical_voltage > 0) && (voltage_used < _params._critical_voltage)) {
        critical_voltage = true;
    } else {
        critical_voltage = false;

    // check capacity failsafe if current monitoring is enabled
    if (has_current() && (_params._critical_capacity > 0) &&
        ((_params._pack_capacity - _state.consumed_mah) < _params._critical_capacity)) {
        critical_capacity = true;
    } else {
        critical_capacity = false;

    if ((voltage_used > 0) && (_params._low_voltage > 0) && (voltage_used < _params._low_voltage)) {
        low_voltage = true;
    } else {
        low_voltage = false;

    // check capacity if current monitoring is enabled
    if (has_current() && (_params._low_capacity > 0) &&
        ((_params._pack_capacity - _state.consumed_mah) < _params._low_capacity)) {
        low_capacity = true;
    } else {
        low_capacity = false;

So, the resting voltage estimate is a constantly evaluated term that should enable additional flight time before a failsafe trigger so long as the voltage and current monitors are correctly set up and accurate.

I’m sorry you continue to fail to accept that my mission with ArduPilot is as a user for commercial operations - not development.

I want to use the firmware to the limits of it’s capability, but I have neither the time nor inclination to regain enough coding proficiency to participate in the code base. I’m grateful to those who do - and as it’s free software I try to re-pay the efforts of the DEV’s by testing, providing feedback, and sharing details of my implementations.

What concerns me is why someone decided to have the firmware always determine the resting state voltage if by default the instantaneous voltage is used for triggering a voltage failsafe.

I appreciate your time and effort to bring some of this code to my attention. But after having written code for decades - decades ago - I’m experienced enough to know that the big picture is often not discernable from a few isolated routines.

I’d greatly benefit from hearing from the people who contributed this aspect to the firmware - and how they intended it to work. I’ll return the favor by doing a through test - and reporting back that it indeed works as intended.

My impression is that very few use the BATT_FS_VOLTRC=1 feature - so it might be good to test to see that it still functions as intended. All I need to know is what exactly was intended.

This is the bulk of the discussion leading up to the feature, largely as it stands today. I found it by clicking on the “History” button when viewing that source file on GitHub.

Improved Battery resistance estimation by rmackay9 · Pull Request #6351 · ArduPilot/ardupilot (

The feature has been included in stable releases for the past 5 years, so the devs should be confident by now that it works as intended. It’s never bad to confirm again, of course, but the feature should likely be considered mature by now.

I use this feature all the time and it works to perfection. RTL consistently at my desired resting voltage of 3.6V per cell upon at rest measured upon landing (slightly higher most the time). It is pretty much included in my default setup of all aircraft.

Allows me to use 3.6V per cell regardless of how much sag the given vehicle+battery combo has - since it is automatically handled by the autopilot. Helps when working with batteries of different age.

1 Like

Also - the reason this is probably not on by default is that it requires a well calibrated current sensor (and voltage). Having a good current sensor is not required for flight (although highly recommended) - but using this feature alongside a bad current sensor will lead to erratic resting voltage calculations that could result in voltage failsafe’s triggering early/late.

1 Like

This is mainly handled by the battery failsafe timer that is defaulted to 10 seconds. So - the voltage must be lower than the set trigger level for 10 seconds to enact a failsafe. Most climbs, gusts, etc. are less than this time period - but the period can be adjusted if needed.

Interesting. Thank you. I use Mauch PL series current sensors on my copters - factory calibrated. Hopefully, this satisfies the requirements.

Where is the time period adjustment made?

BATT_LOW_TIMER is the parameters

Thank you. I’m going to have to read and research all the BATT parameters carefully.

It’s interesting that on my copters BATT_LOW_TIMER=10 is set - for 10 seconds. I didn’t change it, so I expect it’s the default.

The parameter docs mention it’s usefulness on long takeoffs on aircraft with low C batteries. I’ll have to study my voltage and current statistics in my BIN files - but I have a feeling that 10 seconds is more than enough in my case.