Skip to content

Commit cccf0c9

Browse files
committed
pbio/sys/battery: add EV3 battery monitor
Copy the EV3 battery monitor code from [1]. This is something that comes from the original EV3 firmware. [1]: https://github.com/ev3dev/brickd/blob/master/src/power_monitor.vala
1 parent db23327 commit cccf0c9

File tree

4 files changed

+259
-2
lines changed

4 files changed

+259
-2
lines changed

lib/pbio/include/pbio/protocol.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,18 @@ typedef enum {
329329
* @since Pybricks Profile v1.4.0
330330
*/
331331
PBIO_PYBRICKS_STATUS_BLE_HOST_CONNECTED = 9,
332+
/**
333+
* Battery temperature is critically high.
334+
*
335+
* @since Pybricks Profile v1.5.0
336+
*/
337+
PBIO_PYBRICKS_STATUS_BATTERY_HIGH_TEMP_SHUTDOWN = 10,
338+
/**
339+
* Battery temperature is high.
340+
*
341+
* @since Pybricks Profile v1.5.0
342+
*/
343+
PBIO_PYBRICKS_STATUS_BATTERY_HIGH_TEMP_WARNING = 11,
332344
/** Total number of indications. */
333345
NUM_PBIO_PYBRICKS_STATUS,
334346
} pbio_pybricks_status_flags_t;

lib/pbio/platform/ev3/pbsysconfig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#define PBSYS_CONFIG_FEATURE_BUILTIN_USER_PROGRAM_IMU_CALIBRATION (0)
77
#define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6 (1)
88
#define PBSYS_CONFIG_FEATURE_PROGRAM_FORMAT_MULTI_MPY_V6_3_NATIVE (0)
9+
#define PBSYS_CONFIG_BATTERY_TEMP_ESTIMATION (1)
910
#define PBSYS_CONFIG_HMI_NUM_SLOTS (5)
1011
#define PBSYS_CONFIG_HOST (1)
1112
#define PBSYS_CONFIG_HOST_STDIN_BUF_SIZE (21)

lib/pbio/sys/battery.c

Lines changed: 245 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2018-2022 The Pybricks Authors
2+
// Copyright (c) 2018-2025 The Pybricks Authors
33

44
// Provides battery status indication and shutdown on low battery.
55

@@ -8,10 +8,12 @@
88

99
#include <pbdrv/battery.h>
1010
#include <pbio/battery.h>
11+
#include <pbio/os.h>
1112
#include <pbdrv/charger.h>
1213
#include <pbdrv/config.h>
1314
#include <pbdrv/clock.h>
1415
#include <pbdrv/usb.h>
16+
#include <pbsys/config.h>
1517
#include <pbsys/status.h>
1618

1719
// These values are for Alkaline (AA/AAA) batteries
@@ -25,7 +27,211 @@
2527
#define LIION_LOW_MV 6800 // 3.4V per cell
2628
#define LIION_CRITICAL_MV 6000 // 3.0V per cell
2729

30+
// LEGO MINDSTORMS EV3 battery temperature estimation from lms2012
31+
#if PBSYS_CONFIG_BATTERY_TEMP_ESTIMATION
32+
33+
#include <math.h>
34+
#include "../drv/uart/uart_debug_first_port.h"
35+
36+
#define PBSYS_BATTERY_TEMP_TIMER_PERIOD_MS 400
37+
38+
/**
39+
* Function for estimating new battery temperature based on measurements
40+
* of battery voltage and battery current.
41+
* @param [in] V_bat Battery voltage (volts)
42+
* @param [in] I_bat Battery current (amps)
43+
* @returns Estimated battery temperature (degrees Celsius)
44+
*/
45+
static float pbsys_battery_temp_update(float V_bat, float I_bat) {
46+
static struct {
47+
/** Keeps track of sample index since power-on. */
48+
uint32_t index;
49+
/** Running mean current. */
50+
float I_bat_mean;
51+
/** Battery temperature. */
52+
float T_bat;
53+
/** EV3 electronics temperature. */
54+
float T_elec;
55+
/** Old internal resistance of the battery model. */
56+
float R_bat_model_old;
57+
/** Internal resistance of the batteries. */
58+
float R_bat;
59+
// Flag that prevents initialization of R_bat when the battery is charging
60+
bool has_passed_7v5_flag;
61+
} bat_temp;
62+
63+
/*************************** Model parameters *******************************/
64+
// Approx. initial internal resistance of 6 Energizer industrial batteries:
65+
const float R_bat_init = 0.63468f;
66+
// Batteries' heat capacity:
67+
const float heat_cap_bat = 136.6598f;
68+
// Newtonian cooling constant for electronics:
69+
const float K_bat_loss_to_elec = -0.0003f; // -0.000789767;
70+
// Newtonian heating constant for electronics:
71+
const float K_bat_gain_from_elec = 0.001242896f; // 0.001035746;
72+
// Newtonian cooling constant for environment:
73+
const float K_bat_to_room = -0.00012f;
74+
// Battery power Boost
75+
const float battery_power_boost = 1.7f;
76+
// Battery R_bat negative gain
77+
const float R_bat_neg_gain = 1.00f;
78+
79+
// Slope of electronics lossless heating curve (linear!!!) [Deg.C / s]:
80+
const float K_elec_heat_slope = 0.0123175f;
81+
// Newtonian cooling constant for battery packs:
82+
const float K_elec_loss_to_bat = -0.004137487f;
83+
// Newtonian heating constant for battery packs:
84+
const float K_elec_gain_from_bat = 0.002027574f; // 0.00152068;
85+
// Newtonian cooling constant for environment:
86+
const float K_elec_to_room = -0.001931431f; // -0.001843639;
87+
88+
// NB: This time must match PBSYS_BATTERY_TEMP_TIMER_PERIOD_MS
89+
const float sample_period = 0.4f; // Algorithm update period in seconds
90+
91+
float R_bat_model; // Internal resistance of the battery model
92+
float slope_A; // Slope obtained by linear interpolation
93+
float intercept_b; // Offset obtained by linear interpolation
94+
const float I_1A = 0.05f; // Current carrying capacity at bottom of the curve
95+
const float I_2A = 2.0f; // Current carrying capacity at the top of the curve
96+
97+
float R_1A; // Internal resistance of the batteries at 1A and V_bat
98+
float R_2A; // Internal resistance of the batteries at 2A and V_bat
99+
100+
float dT_bat_own; // Batteries' own heat
101+
float dT_bat_loss_to_elec; // Batteries' heat loss to electronics
102+
float dT_bat_gain_from_elec; // Batteries' heat gain from electronics
103+
float dT_bat_loss_to_room; // Batteries' cooling from environment
104+
105+
float dT_elec_own; // Electronics' own heat
106+
float dT_elec_loss_to_bat; // Electronics' heat loss to the battery pack
107+
float dT_elec_gain_from_bat; // Electronics' heat gain from battery packs
108+
float dT_elec_loss_to_room; // Electronics' heat loss to the environment
109+
110+
/***************************************************************************/
111+
112+
// Update the average current: I_bat_mean
113+
if (bat_temp.index > 0) {
114+
bat_temp.I_bat_mean = (bat_temp.index * bat_temp.I_bat_mean + I_bat) / (bat_temp.index + 1);
115+
} else {
116+
bat_temp.I_bat_mean = I_bat;
117+
}
118+
119+
bat_temp.index++;
120+
121+
// Calculate R_1A as a function of V_bat (internal resistance at 1A continuous)
122+
R_1A = 0.014071f * (V_bat * V_bat * V_bat * V_bat)
123+
- 0.335324f * (V_bat * V_bat * V_bat)
124+
+ 2.933404f * (V_bat * V_bat)
125+
- 11.243047f * V_bat
126+
+ 16.897461f;
127+
128+
// Calculate R_2A as a function of V_bat (internal resistance at 2A continuous)
129+
R_2A = 0.014420f * (V_bat * V_bat * V_bat * V_bat)
130+
- 0.316728f * (V_bat * V_bat * V_bat)
131+
+ 2.559347f * (V_bat * V_bat)
132+
- 9.084076f * V_bat
133+
+ 12.794176f;
134+
135+
// Calculate the slope by linear interpolation between R_1A and R_2A
136+
slope_A = (R_1A - R_2A) / (I_1A - I_2A);
137+
138+
// Calculate intercept by linear interpolation between R1_A and R2_A
139+
intercept_b = R_1A - slope_A * R_1A;
140+
141+
// Reload R_bat_model:
142+
R_bat_model = slope_A * bat_temp.I_bat_mean + intercept_b;
143+
144+
// Calculate batteries' internal resistance: R_bat
145+
if (V_bat > 7.5 && !bat_temp.has_passed_7v5_flag) {
146+
bat_temp.R_bat = R_bat_init; // 7.5 V not passed a first time
147+
} else {
148+
// Only update R_bat with positive outcomes: R_bat_model - R_bat_model_old
149+
// R_bat updated with the change in model R_bat is not equal value in the model!
150+
if ((R_bat_model - bat_temp.R_bat_model_old) > 0) {
151+
bat_temp.R_bat += R_bat_model - bat_temp.R_bat_model_old;
152+
} else { // The negative outcome of R_bat_model added to only part of R_bat
153+
bat_temp.R_bat += R_bat_neg_gain * (R_bat_model - bat_temp.R_bat_model_old);
154+
}
155+
// Make sure we initialize R_bat later
156+
bat_temp.has_passed_7v5_flag = true;
157+
}
158+
159+
// Save R_bat_model for use in the next function call
160+
bat_temp.R_bat_model_old = R_bat_model;
161+
162+
// pbdrv_uart_debug_printf("%c %f %f %f %f %f %f\r\n", bat_temp.has_passed_7v5_flag ? 'Y' : 'N',
163+
// (double)R_1A, (double)R_2A, (double)slope_A, (double)intercept_b,
164+
// (double)(R_bat_model - bat_temp.R_bat_model_old), (double)bat_temp.R_bat);
165+
166+
/**** Calculate the 4 types of temperature change for the batteries ****/
167+
168+
// Calculate the batteries' own temperature change
169+
dT_bat_own = bat_temp.R_bat * I_bat * I_bat * sample_period * battery_power_boost / heat_cap_bat;
170+
171+
// Calculate the batteries' heat loss to the electronics
172+
if (bat_temp.T_bat > bat_temp.T_elec) {
173+
dT_bat_loss_to_elec = K_bat_loss_to_elec * (bat_temp.T_bat - bat_temp.T_elec) * sample_period;
174+
} else {
175+
dT_bat_loss_to_elec = 0.0f;
176+
}
177+
178+
// Calculate the batteries' heat gain from the electronics
179+
if (bat_temp.T_bat < bat_temp.T_elec) {
180+
dT_bat_gain_from_elec = K_bat_gain_from_elec * (bat_temp.T_elec - bat_temp.T_bat) * sample_period;
181+
} else {
182+
dT_bat_gain_from_elec = 0.0f;
183+
}
184+
185+
// Calculate the batteries' heat loss to environment
186+
dT_bat_loss_to_room = K_bat_to_room * bat_temp.T_bat * sample_period;
187+
188+
/**** Calculate the 4 types of temperature change for the electronics ****/
189+
190+
// Calculate the electronics' own temperature change
191+
dT_elec_own = K_elec_heat_slope * sample_period;
192+
193+
// Calculate the electronics' heat loss to the batteries
194+
if (bat_temp.T_elec > bat_temp.T_bat) {
195+
dT_elec_loss_to_bat = K_elec_loss_to_bat * (bat_temp.T_elec - bat_temp.T_bat) * sample_period;
196+
} else {
197+
dT_elec_loss_to_bat = 0.0f;
198+
}
199+
200+
// Calculate the electronics' heat gain from the batteries
201+
if (bat_temp.T_elec < bat_temp.T_bat) {
202+
dT_elec_gain_from_bat = K_elec_gain_from_bat * (bat_temp.T_bat - bat_temp.T_elec) * sample_period;
203+
} else {
204+
dT_elec_gain_from_bat = 0.0f;
205+
}
206+
207+
// Calculate the electronics' heat loss to the environment
208+
dT_elec_loss_to_room = K_elec_to_room * bat_temp.T_elec * sample_period;
209+
210+
/*****************************************************************************/
211+
212+
// pbdrv_uart_debug_printf("%f %f %f %f %f <> %f %f %f %f %f\r\n",
213+
// (double)dT_bat_own, (double)dT_bat_loss_to_elec,
214+
// (double)dT_bat_gain_from_elec, (double)dT_bat_loss_to_room, (double)bat_temp.T_bat,
215+
// (double)dT_elec_own, (double)dT_elec_loss_to_bat, (double)dT_elec_gain_from_bat,
216+
// (double)dT_elec_loss_to_room, (double)bat_temp.T_elec);
217+
218+
// Refresh battery temperature
219+
bat_temp.T_bat += dT_bat_own + dT_bat_loss_to_elec + dT_bat_gain_from_elec + dT_bat_loss_to_room;
220+
221+
// Refresh electronics temperature
222+
bat_temp.T_elec += dT_elec_own + dT_elec_loss_to_bat + dT_elec_gain_from_bat + dT_elec_loss_to_room;
223+
224+
return bat_temp.T_bat;
225+
}
226+
227+
static pbio_os_timer_t pbsys_battery_temp_timer;
228+
229+
#endif // PBSYS_CONFIG_BATTERY_TEMP_ESTIMATION
230+
28231
void pbsys_battery_init(void) {
232+
#if PBSYS_CONFIG_BATTERY_TEMP_ESTIMATION
233+
pbio_os_timer_set(&pbsys_battery_temp_timer, PBSYS_BATTERY_TEMP_TIMER_PERIOD_MS);
234+
#endif
29235
}
30236

31237
/**
@@ -60,6 +266,44 @@ void pbsys_battery_poll(void) {
60266
if (pbsys_status_test_debounce(PBIO_PYBRICKS_STATUS_BATTERY_LOW_VOLTAGE_SHUTDOWN, true, 3000)) {
61267
pbsys_status_set(PBIO_PYBRICKS_STATUS_SHUTDOWN_REQUEST);
62268
}
269+
270+
#if PBSYS_CONFIG_BATTERY_TEMP_ESTIMATION
271+
if (!is_liion && pbio_os_timer_is_expired(&pbsys_battery_temp_timer)) {
272+
pbio_os_timer_extend(&pbsys_battery_temp_timer);
273+
274+
uint16_t voltage_mv;
275+
uint16_t current_ma;
276+
if (pbdrv_battery_get_voltage_now(&voltage_mv) == PBIO_SUCCESS &&
277+
pbdrv_battery_get_current_now(&current_ma) == PBIO_SUCCESS) {
278+
279+
static float old_temp;
280+
float new_temp = pbsys_battery_temp_update(voltage_mv / 1000.0f, current_ma / 1000.0f);
281+
if (fabsf(new_temp - old_temp) > 0.1f) {
282+
old_temp = new_temp;
283+
}
284+
285+
const float high_temp_warning = 25.0f;
286+
const float high_temp_critical = 30.0f;
287+
288+
if (old_temp >= high_temp_warning) {
289+
pbsys_status_set(PBIO_PYBRICKS_STATUS_BATTERY_HIGH_TEMP_WARNING);
290+
} else if (old_temp < high_temp_warning) {
291+
pbsys_status_clear(PBIO_PYBRICKS_STATUS_BATTERY_HIGH_TEMP_WARNING);
292+
}
293+
294+
if (old_temp >= high_temp_critical) {
295+
pbsys_status_set(PBIO_PYBRICKS_STATUS_BATTERY_HIGH_TEMP_SHUTDOWN);
296+
} else if (old_temp < high_temp_critical) {
297+
pbsys_status_clear(PBIO_PYBRICKS_STATUS_BATTERY_HIGH_TEMP_SHUTDOWN);
298+
}
299+
300+
// Shut down on high temperature so we don't damage AAA batteries.
301+
if (pbsys_status_test_debounce(PBIO_PYBRICKS_STATUS_BATTERY_HIGH_TEMP_SHUTDOWN, true, 3000)) {
302+
pbsys_status_set(PBIO_PYBRICKS_STATUS_SHUTDOWN_REQUEST);
303+
}
304+
}
305+
}
306+
#endif
63307
}
64308

65309
/**

lib/pbio/sys/light.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ void pbsys_status_light_handle_status_change(void) {
192192
} else if (pbsys_status_test(PBIO_PYBRICKS_STATUS_SHUTDOWN_REQUEST)) {
193193
warning_indication = PBSYS_STATUS_LIGHT_INDICATION_WARNING_SHUTDOWN_REQUESTED;
194194
#endif
195-
} else if (pbsys_status_test(PBIO_PYBRICKS_STATUS_BATTERY_HIGH_CURRENT)) {
195+
} else if (pbsys_status_test(PBIO_PYBRICKS_STATUS_BATTERY_HIGH_CURRENT) || pbsys_status_test(PBIO_PYBRICKS_STATUS_BATTERY_HIGH_TEMP_WARNING)) {
196196
warning_indication = PBSYS_STATUS_LIGHT_INDICATION_WARNING_HIGH_CURRENT;
197197
} else if (pbsys_status_test(PBIO_PYBRICKS_STATUS_BATTERY_LOW_VOLTAGE_WARNING)) {
198198
warning_indication = PBSYS_STATUS_LIGHT_INDICATION_WARNING_LOW_VOLTAGE;

0 commit comments

Comments
 (0)