feat(core): recover battery_critical flag on soc threshold.
What changed, and why it matters
This commit changes how a Trezor hardware wallet decides whether its battery is dangerously low and when it is safe to turn on again. Previously the device used raw battery voltage thresholds and a smoothed voltage estimate. Now it relies mainly on the fuel gauge's reported state-of-charge (SOC) percentage and whether USB power is connected. The change removes some voltage-based recovery thresholds and a smoothing variable. It is a power-management refinement, not an obvious security fix, but it could affect device availability and safe shutdown behavior.
Review the fuel gauge SOC accuracy near 0-2% and verify that the new recovery logic cannot allow boot or operation with an actually depleted battery. Confirm that removing the wireless charging voltage threshold does not create unsafe startup conditions on wireless power. Consider adding a changelog entry because this is a user-visible safety/availability change.
Security signals we found
Change in safety-critical power-state decision logic
Removal of voltage-based recovery thresholds in favor of SOC-based recovery
Addition of USB-connected condition before marking battery critical
Removal of smoothed vbat_tau used for undervoltage detection
No changelog entry despite safety-relevant behavioral change
Evidence from the diff
The patch modifies the STM32U5 power manager in Trezor firmware. In power_manager.c, pm_turn_on() no longer checks USB/wireless connection status or raw vbat recovery thresholds before deciding the device cannot start; it now only checks the battery_critical flag. In power_monitoring.c, the undervoltage detection now uses instantaneous pmic_data.vbat instead of the smoothed vbat_tau, adds a !drv->usb_connected condition before marking battery critical, and recovers from battery_critical when fuel_gauge.soc_latched >= 2% or USB is connected. The header replaces PM_BATTERY_UNDERVOLT_RECOVERY_THR_V and PM_BATTERY_UNDERVOLT_RECOVERY_WPC_THR_V with PM_BATTERY_CRITICAL_RECOVERY_SOC (0.02) and removes the vbat_tau field from the driver struct.
Changed components
core/embed/sys/power_manager/stm32u5/power_manager.ccore/embed/sys/power_manager/stm32u5/power_manager_internal.hcore/embed/sys/power_manager/stm32u5/power_monitoring.cInspect captured patch +9 / −20
diff --git a/core/embed/sys/power_manager/stm32u5/power_manager.c b/core/embed/sys/power_manager/stm32u5/power_manager.c
index 51d4c73b8..90534817c 100644
--- a/core/embed/sys/power_manager/stm32u5/power_manager.c
+++ b/core/embed/sys/power_manager/stm32u5/power_manager.c
@@ -335,15 +335,8 @@ pm_status_t pm_turn_on(void) {
irq_unlock(irq_key);
} while (pmic_last_update_us == 0);
- bool usb_connected = drv->usb_connected;
- bool wireless_connected =
- drv->wireless_connected &&
- drv->pmic_data.vbat > PM_BATTERY_UNDERVOLT_RECOVERY_WPC_THR_V;
-
// Check if device has enough power to startup
- if ((!usb_connected && !wireless_connected) &&
- (drv->pmic_data.vbat < PM_BATTERY_UNDERVOLT_RECOVERY_THR_V ||
- drv->battery_critical)) {
+ if (drv->battery_critical) {
irq_key_t irq_key = irq_lock();
drv->battery_critical = true;
pm_store_data_to_backup_ram();
diff --git a/core/embed/sys/power_manager/stm32u5/power_manager_internal.h b/core/embed/sys/power_manager/stm32u5/power_manager_internal.h
index bc56cd25b..ccc75fceb 100644
--- a/core/embed/sys/power_manager/stm32u5/power_manager_internal.h
+++ b/core/embed/sys/power_manager/stm32u5/power_manager_internal.h
@@ -31,8 +31,7 @@
#define PM_TIMER_PERIOD_MS 100
#define PM_SHUTDOWN_TIMEOUT_MS 15000
#define PM_BATTERY_UNDERVOLT_THR_V 3.0f
-#define PM_BATTERY_UNDERVOLT_RECOVERY_THR_V 3.1f
-#define PM_BATTERY_UNDERVOLT_RECOVERY_WPC_THR_V 3.2f
+#define PM_BATTERY_CRITICAL_RECOVERY_SOC 0.02f
#define PM_BATTERY_LOW_THRESHOLD_SOC 15
#define PM_BATTERY_CHARGING_CURRENT_MAX PMIC_CHARGING_LIMIT_MAX
#define PM_BATTERY_CHARGING_CURRENT_MIN PMIC_CHARGING_LIMIT_MIN
@@ -89,8 +88,6 @@ typedef struct {
uint8_t bat_sampling_buf_head_idx;
uint8_t soc_ceiled;
- float vbat_tau;
-
uint8_t soc_target;
bool soc_target_reached;
float target_battery_ocv_v_tau;
diff --git a/core/embed/sys/power_manager/stm32u5/power_monitoring.c b/core/embed/sys/power_manager/stm32u5/power_monitoring.c
index 9d08c83ce..df7b5d696 100644
--- a/core/embed/sys/power_manager/stm32u5/power_monitoring.c
+++ b/core/embed/sys/power_manager/stm32u5/power_monitoring.c
@@ -57,16 +57,12 @@ void pm_pmic_data_ready(void* context, pmic_report_t* report) {
// Store measurement timestamp
if (drv->pmic_last_update_us == 0) {
drv->pmic_sampling_period_ms = PM_TIMER_PERIOD_MS;
- drv->vbat_tau = report->vbat;
} else {
// Calculate the time since the last PMIC update
drv->pmic_sampling_period_ms =
(systick_us() - drv->pmic_last_update_us) / 1000;
}
drv->pmic_last_update_us = systick_us();
-
- drv->vbat_tau = (drv->vbat_tau * 0.95f) + (report->vbat * 0.05f);
-
// Copy pmic data
memcpy(&drv->pmic_data, report, sizeof(pmic_report_t));
@@ -301,13 +297,16 @@ static void pm_parse_power_source_state(pm_driver_t* drv) {
}
// Check battery voltage for critical (undervoltage) threshold
- if ((drv->vbat_tau < PM_BATTERY_UNDERVOLT_THR_V) && !drv->battery_critical) {
+ if ((drv->pmic_data.vbat < PM_BATTERY_UNDERVOLT_THR_V) &&
+ !drv->battery_critical && !drv->usb_connected) {
// Force Fuel gauge to 0, keep the covariance
fuel_gauge_set_soc(&drv->fuel_gauge, 0.0f, drv->fuel_gauge.P);
-
drv->battery_critical = true;
- } else if (drv->vbat_tau > (PM_BATTERY_UNDERVOLT_RECOVERY_THR_V) &&
- drv->battery_critical) {
+
+ } else if (drv->fuel_gauge.soc_latched >=
+ (PM_BATTERY_CRITICAL_RECOVERY_SOC) ||
+ drv->usb_connected) {
+ // Restore the battery critical state
drv->battery_critical = false;
}
}
Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.