LCOV - code coverage report
Current view: top level - SITL - SIM_Battery.cpp (source / functions) Hit Total Coverage
Test: lcov.info Lines: 74 75 98.7 %
Date: 2026-09-09 11:02:08 Functions: 6 6 100.0 %

          Line data    Source code
       1             : /*
       2             :    This program is free software: you can redistribute it and/or modify
       3             :    it under the terms of the GNU General Public License as published by
       4             :    the Free Software Foundation, either version 3 of the License, or
       5             :    (at your option) any later version.
       6             : 
       7             :    This program is distributed in the hope that it will be useful,
       8             :    but WITHOUT ANY WARRANTY; without even the implied warranty of
       9             :    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      10             :    GNU General Public License for more details.
      11             : 
      12             :    You should have received a copy of the GNU General Public License
      13             :    along with this program.  If not, see <http://www.gnu.org/licenses/>.
      14             :  */
      15             : /*
      16             :   battery model for electric aircraft
      17             : */
      18             : 
      19             : #include "SIM_Battery.h"
      20             : #include <float.h>
      21             : #include <AP_Math/AP_Math.h>
      22             : 
      23             : using namespace SITL;
      24             : 
      25             : /*
      26             :   state of charge table for a single cell battery.
      27             :  */
      28             : static const struct {
      29             :     float volt_per_cell;
      30             :     float soc_pct;
      31             : } soc_table[] = {
      32             :     { 4.173, 100 },
      33             :     { 4.112, 96.15 },
      34             :     { 4.085, 92.31 },
      35             :     { 4.071, 88.46 },
      36             :     { 4.039, 84.62 },
      37             :     { 3.987, 80.77 },
      38             :     { 3.943, 76.92 },
      39             :     { 3.908, 73.08 },
      40             :     { 3.887, 69.23 },
      41             :     { 3.854, 65.38 },
      42             :     { 3.833, 61.54 },
      43             :     { 3.801, 57.69 },
      44             :     { 3.783, 53.85 },
      45             :     { 3.742, 50 },
      46             :     { 3.715, 46.15 },
      47             :     { 3.679, 42.31 },
      48             :     { 3.636, 38.46 },
      49             :     { 3.588, 34.62 },
      50             :     { 3.543, 30.77 },
      51             :     { 3.503, 26.92 },
      52             :     { 3.462, 23.08 },
      53             :     { 3.379, 19.23 },
      54             :     { 3.296, 15.38 },
      55             :     { 3.218, 11.54 },
      56             :     { 3.165, 7.69 },
      57             :     { 3.091, 3.85 },
      58             :     { 2.977, 2.0 },
      59             :     { 2.8,   1.5 },
      60             :     { 2.7,   1.3 },
      61             :     { 2.5,   1.2 },
      62             :     { 2.3,   1.1 },
      63             :     { 2.1,   1.0 },
      64             :     { 1.9,   0.9 },
      65             :     { 1.6,   0.8 },
      66             :     { 1.3,   0.7 },
      67             :     { 1.0,   0.6 },
      68             :     { 0.6,   0.4 },
      69             :     { 0.3,   0.2 },
      70             :     { 0.01,  0.01},
      71             :     { 0.001, 0.001 }};
      72             : 
      73             : constexpr float maximum_permissible_dt = 0.1f; // seconds
      74             : 
      75             : /*
      76             :   use table to get resting voltage from remaining capacity
      77             :  */
      78   177104555 : float Battery::get_resting_voltage(void) const
      79             : {
      80   177104555 :     if (capacity_is_unlimited()) {
      81   176312215 :         return voltage_set;
      82             :     }
      83      792340 :     const float charge_pct = 100 * remaining_Ah / capacity_Ah;
      84      792340 :     const float max_cell_voltage = soc_table[0].volt_per_cell;
      85      792340 :     const float min_cell_voltage = soc_table[ARRAY_SIZE(soc_table) - 1].volt_per_cell;
      86     1156389 :     for (uint8_t i=1; i<ARRAY_SIZE(soc_table); i++) {
      87     1156389 :         if (charge_pct >= soc_table[i].soc_pct) {
      88             :             // linear interpolation between table rows
      89      792340 :             const float dv1 = charge_pct - soc_table[i].soc_pct;
      90      792340 :             const float dv2 = soc_table[i-1].soc_pct - soc_table[i].soc_pct;
      91      792340 :             const float vpc1 = soc_table[i].volt_per_cell;
      92      792340 :             const float vpc2 = soc_table[i-1].volt_per_cell;
      93      792340 :             const float cell_volt = vpc1 + (dv1 / dv2) * (vpc2 - vpc1);
      94      792340 :             return (cell_volt / max_cell_voltage) * max_voltage;
      95             :         }
      96             :     }
      97             :     // off the bottom of the table
      98           0 :     return min_cell_voltage;
      99             : }
     100             : 
     101             : /*
     102             :   return remaining Amp-hours (aka "charge", "state of charge") corresponding to a voltage
     103             :  */
     104        3437 : float Battery::compute_remaining_Ah(float voltage) const
     105             : {
     106        3437 :     if (capacity_is_unlimited()) {
     107        3330 :         return FLT_MAX;
     108             :     }
     109             : 
     110         107 :     const float max_cell_voltage = soc_table[0].volt_per_cell;
     111         107 :     const float cell_volt = (voltage / max_voltage) * max_cell_voltage;
     112             : 
     113         404 :     for (uint8_t i=1; i<ARRAY_SIZE(soc_table); i++) {
     114         398 :         if (cell_volt >= soc_table[i].volt_per_cell) {
     115             :             // linear interpolation between table rows
     116         101 :             const float dv1 = cell_volt - soc_table[i].volt_per_cell;
     117         101 :             const float dv2 = soc_table[i-1].volt_per_cell - soc_table[i].volt_per_cell;
     118         101 :             const float soc1 = soc_table[i].soc_pct;
     119         101 :             const float soc2 = soc_table[i-1].soc_pct;
     120         101 :             const float soc = soc1 + (dv1 / dv2) * (soc2 - soc1);
     121         101 :             return capacity_Ah * (soc * 0.01);
     122             :         }
     123             :     }
     124             :     // off the bottom of the table
     125           6 :     return 0.0f;
     126             : }
     127             : 
     128             : // Reminder: capacity <= 0 means **unlimited**
     129        3345 : void Battery::setup(float _capacity_Ah, float _resistance_ohm, float _max_voltage, float _ambient_temperature_degC)
     130             : {
     131        3345 :     capacity_Ah = _capacity_Ah;
     132        3345 :     resistance_ohm = _resistance_ohm;
     133        3345 :     max_voltage = _max_voltage;
     134        3345 :     ambient_temperature_degC = _ambient_temperature_degC;
     135             : 
     136        3345 :     voltage_set = max_voltage;
     137        3345 :     voltage_filter.reset(voltage_set);
     138        3345 :     remaining_Ah = compute_remaining_Ah(voltage_set);
     139        3345 : }
     140             : 
     141             : // A negative value for desired_resistance means "no change".
     142   176991608 : void Battery::maybe_reset(float desired_voltage, float desired_capacity_Ah, float desired_resistance_ohm)
     143             : {
     144   176991608 :     if (!is_negative(desired_resistance_ohm)) {
     145     1559791 :         resistance_ohm = desired_resistance_ohm;
     146             :     }
     147             : 
     148   176991608 :     const bool reset_not_needed = (is_equal(voltage_set, desired_voltage)
     149   176991608 :                                    && is_equal(capacity_Ah, desired_capacity_Ah));
     150   176991608 :     if (reset_not_needed) {
     151   176991516 :         return;
     152             :     }
     153             : 
     154          92 :     capacity_Ah = desired_capacity_Ah;
     155             :     // a negative desired voltage is unexpected, but not problematic
     156          92 :     voltage_set = MIN(desired_voltage, max_voltage);
     157          92 :     voltage_filter.reset(voltage_set);
     158          92 :     remaining_Ah = compute_remaining_Ah(voltage_set);
     159             : }
     160             : 
     161   177104631 : void Battery::consume_energy(float attempted_current_amp, uint64_t now_us)
     162             : {
     163   177104631 :     constexpr float microsec_to_sec = 1.0e-6f;
     164   177104631 :     const float dt = static_cast<float>(now_us - last_us) * microsec_to_sec;
     165   177104631 :     if (dt <= 0.0f) {
     166          76 :         return;
     167             :     }
     168   177104611 :     last_us = now_us;
     169   177104611 :     if (dt > maximum_permissible_dt) {
     170          56 :         return;
     171             :     }
     172   177104555 :     constexpr float hours_per_second = 1.0f / 3600.0f;
     173   177104555 :     const float dt_hr = dt * hours_per_second;
     174   177104555 :     const float delta_Ah = MIN(attempted_current_amp * dt_hr, remaining_Ah);
     175   177104555 :     if (!capacity_is_unlimited()) {
     176      792340 :         remaining_Ah -= delta_Ah;
     177             :     }
     178   177104555 :     const float current_amp = delta_Ah / dt_hr;
     179   177104555 :     const float voltage_delta = current_amp * resistance_ohm;
     180   177104555 :     const float sagged_voltage = get_resting_voltage() - voltage_delta;
     181   177104555 :     voltage_filter.apply(sagged_voltage, dt);
     182             : 
     183   177104555 :     update_temperature(current_amp, dt);
     184             : }
     185             : 
     186             : // A first-order temperature growth & decay model
     187   177104555 : void Battery::update_temperature(float current_amp, float dt)
     188             : {
     189             :     // Reasonable thermal_capacity value for a commonly sized (500g) Li-ion battery
     190             :     // (reminder: thermal_capacity = mass * specific_heat)
     191   177104555 :     constexpr float thermal_capacity = 500.0f;  // J/degC
     192   177104555 :     constexpr float inverse_of_thermal_capacity = 1 / thermal_capacity;  // use inverse so we can multiply, not divide
     193   177104555 :     const float temp_increase = (current_amp * current_amp) * resistance_ohm * inverse_of_thermal_capacity * dt;
     194             :     // Account for ambient dissipation. Rate chosen to match previous (unjustified) steady-state behavior
     195   177104555 :     constexpr float temperature_decay_coefficient = 5.6e-4f;
     196   177104555 :     const float temp_decrease = (temperature_degC - ambient_temperature_degC) * temperature_decay_coefficient * dt;
     197   177104555 :     temperature_degC += (temp_increase - temp_decrease);
     198   177104555 : }

Generated by: LCOV version 1.14