feat(core): detect battery disconnection
What changed, and why it matters
This commit adds a new hardware safety feature to Trezor firmware that detects when a device's battery appears to be physically disconnected (by checking if the battery voltage drops below 0.5 V). When disconnection is detected, the firmware now reports a 'Battery Disconnected' error and disables charging. It also fixes an unrelated bug where a temperature-control event flag was not being exposed to the user interface. There is no indication in the commit that this is a security fix for an exploitable vulnerability; it reads as a normal product-safety and diagnostics improvement.
Treat as a routine firmware improvement rather than a security patch. Reviewers may want to confirm that the 0.5 V / 0.8 V thresholds are appropriate for the hardware and that disabling charging on a disconnected battery does not interfere with factory testing or USB-only operation. No urgent user action is indicated.
Security signals we found
New hardware-fault detection path added (battery disconnection)
Charging is disabled when battery disconnection is detected
Event flag bit positions shifted; bindings updated consistently
No changelog entry despite functional change
Evidence from the diff
The change introduces battery disconnection detection in the STM32U5 power manager. A new battery_disconnected latch is set when PMIC-reported VBAT falls below 0.5 V, and clears only when VBAT rises above 0.8 V (hysteresis). The state is exposed through pm_get_state() and a new battery_connected_changed event flag is added to the power-manager event union. Rust and MicroPython bindings are updated, and the production-test welcome screen now shows ‘Battery Disconnected’ and calls charging_disable() when the event fires while disconnected. The commit also corrects the bit positions of existing event constants because a previously missing temp_control_active_changed flag is now included, shifting subsequent flags by one bit.
Changed components
core/embed/io/power_manager (STM32U5 power monitoring and state reporting)core/embed/rust/src/trezorhal/power_manager.rs (Rust HAL bindings)core/embed/rust/src/ui/event/power_manager.rs (UI event parsing)core/embed/rust/src/ui/layout_eckhart/prodtest/welcome.rs (production-test screen)core/embed/upymod/modtrezorio/modtrezorio-pm.h (MicroPython event constants)Inspect captured patch +53 / −10
diff --git a/core/embed/io/power_manager/inc/io/power_manager.h b/core/embed/io/power_manager/inc/io/power_manager.h
index 8a169b628..f6cd7985a 100644
--- a/core/embed/io/power_manager/inc/io/power_manager.h
+++ b/core/embed/io/power_manager/inc/io/power_manager.h
@@ -60,6 +60,7 @@ typedef union {
bool ntc_connected_changed : 1;
bool charging_limited_changed : 1;
bool temp_control_active_changed : 1;
+ bool battery_connected_changed : 1;
// Jump detection events (fast changes within a short time window)
bool battery_temp_jump_detected : 1;
@@ -75,6 +76,7 @@ typedef struct {
bool ntc_connected;
bool charging_limited;
bool temp_control_active;
+ bool battery_connected;
pm_charging_status_t charging_status;
pm_power_status_t power_status;
uint8_t soc;
diff --git a/core/embed/io/power_manager/power_manager_poll.c b/core/embed/io/power_manager/power_manager_poll.c
index 0d411f0b4..c4117b694 100644
--- a/core/embed/io/power_manager/power_manager_poll.c
+++ b/core/embed/io/power_manager/power_manager_poll.c
@@ -155,6 +155,10 @@ static bool pm_fsm_update(pm_fsm_t* fsm, pm_state_t* new_state) {
fsm->events.flags.charging_limited_changed = true;
}
+ if (new_state->battery_connected != fsm->last_state.battery_connected) {
+ fsm->events.flags.battery_connected_changed = true;
+ }
+
fsm->last_state = *new_state;
return fsm->events.all != 0;
diff --git a/core/embed/io/power_manager/stm32u5/power_manager.c b/core/embed/io/power_manager/stm32u5/power_manager.c
index ab3a1d077..22417b7f8 100644
--- a/core/embed/io/power_manager/stm32u5/power_manager.c
+++ b/core/embed/io/power_manager/stm32u5/power_manager.c
@@ -259,7 +259,7 @@ pm_status_t pm_get_state(pm_state_t* state) {
}
state->charging_limited = drv->charging_limited_latched;
-
+ state->battery_connected = !drv->battery_disconnected;
state->power_status = drv->state;
state->soc = drv->soc_ceiled;
state->battery_temp = drv->pmic_data.ntc_temp;
diff --git a/core/embed/io/power_manager/stm32u5/power_manager_internal.h b/core/embed/io/power_manager/stm32u5/power_manager_internal.h
index c12f5868a..1c91a2d29 100644
--- a/core/embed/io/power_manager/stm32u5/power_manager_internal.h
+++ b/core/embed/io/power_manager/stm32u5/power_manager_internal.h
@@ -37,6 +37,10 @@
#define PM_BATTERY_CHARGING_CURRENT_MAX PMIC_CHARGING_LIMIT_MAX
#define PM_BATTERY_CHARGING_CURRENT_MIN PMIC_CHARGING_LIMIT_MIN
+#define PM_BATTERY_DISCONNECTED_THR_V 0.5f // battery disconnection detection
+#define PM_BATTERY_DISCONNECTED_REC_V \
+ 0.8f // recovery from disconnect detection
+
#define PM_SELF_DISG_RATE_HIBERNATION_MA 0.004f
#define PM_SELF_DISG_RATE_SUSPEND_MA 0.032f
@@ -88,6 +92,9 @@ typedef struct {
bool charging_limited_latched;
uint32_t charging_limited_start_ms;
+ // battery disconnection detection, voltage based
+ bool battery_disconnected;
+
#ifdef PM_ENABLE_TEMP_CONTROL
// Temp controller
uint32_t temp_control_timeout;
diff --git a/core/embed/io/power_manager/stm32u5/power_monitoring.c b/core/embed/io/power_manager/stm32u5/power_monitoring.c
index 93a1bc6ca..027929c45 100644
--- a/core/embed/io/power_manager/stm32u5/power_monitoring.c
+++ b/core/embed/io/power_manager/stm32u5/power_monitoring.c
@@ -84,6 +84,12 @@ void pm_pmic_data_ready(void* context, pmic_report_t* report) {
telemetry_update_battery_temp(drv->pmic_data.ntc_temp);
#endif
+ // detect battery disconnection
+ drv->battery_disconnected =
+ (drv->pmic_data.vbat < PM_BATTERY_DISCONNECTED_THR_V) ||
+ (drv->battery_disconnected &&
+ drv->pmic_data.vbat < PM_BATTERY_DISCONNECTED_REC_V);
+
pm_parse_power_source_state(drv);
// Run battery charging controller
diff --git a/core/embed/rust/src/trezorhal/power_manager.rs b/core/embed/rust/src/trezorhal/power_manager.rs
index 19e88bf11..24077b7c1 100644
--- a/core/embed/rust/src/trezorhal/power_manager.rs
+++ b/core/embed/rust/src/trezorhal/power_manager.rs
@@ -22,6 +22,8 @@ pub fn pm_parse_event(event: ffi::pm_event_t) -> PMEvent {
pm_event.charging_limited_changed = event.flags.charging_limited_changed();
pm_event.soc_updated = event.flags.soc_updated();
pm_event.battery_temp_jump_detected = event.flags.battery_temp_jump_detected();
+ pm_event.temp_control_active_changed = event.flags.temp_control_active_changed();
+ pm_event.battery_connected_changed = event.flags.battery_connected_changed();
pm_event.battery_ocv_jump_detected = event.flags.battery_ocv_jump_detected();
pm_event.charging_status_changed = event.flags.charging_status_changed();
pm_event.power_status_changed = event.flags.power_status_changed();
@@ -64,6 +66,12 @@ pub fn is_charging_limited() -> bool {
state.charging_limited
}
+pub fn is_battery_connected() -> bool {
+ let mut state: ffi::pm_state_t = unsafe { core::mem::zeroed() };
+ unsafe { ffi::pm_get_state(&mut state as _) };
+ state.battery_connected
+}
+
pub fn suspend() {
unsafe { ffi::pm_suspend(null_mut()) };
}
diff --git a/core/embed/rust/src/ui/event/power_manager.rs b/core/embed/rust/src/ui/event/power_manager.rs
index 7ecd77b3e..6ab64ec16 100644
--- a/core/embed/rust/src/ui/event/power_manager.rs
+++ b/core/embed/rust/src/ui/event/power_manager.rs
@@ -7,6 +7,8 @@ pub struct PMEvent {
pub wireless_connected_changed: bool,
pub ntc_connected_changed: bool,
pub charging_limited_changed: bool,
+ pub temp_control_active_changed: bool,
+ pub battery_connected_changed: bool,
pub battery_temp_jump_detected: bool,
pub battery_ocv_jump_detected: bool,
pub soc_updated: bool,
@@ -21,9 +23,11 @@ impl PMEvent {
wireless_connected_changed: (flags & (1 << 3)) != 0,
ntc_connected_changed: (flags & (1 << 4)) != 0,
charging_limited_changed: (flags & (1 << 5)) != 0,
- battery_temp_jump_detected: (flags & (1 << 6)) != 0,
- battery_ocv_jump_detected: (flags & (1 << 7)) != 0,
- soc_updated: (flags & (1 << 8)) != 0,
+ temp_control_active_changed: (flags & (1 << 6)) != 0,
+ battery_connected_changed: (flags & (1 << 7)) != 0,
+ battery_temp_jump_detected: (flags & (1 << 8)) != 0,
+ battery_ocv_jump_detected: (flags & (1 << 9)) != 0,
+ soc_updated: (flags & (1 << 10)) != 0,
}
}
}
diff --git a/core/embed/rust/src/ui/layout_eckhart/prodtest/welcome.rs b/core/embed/rust/src/ui/layout_eckhart/prodtest/welcome.rs
index 350bd3f98..9aa05972d 100644
--- a/core/embed/rust/src/ui/layout_eckhart/prodtest/welcome.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/prodtest/welcome.rs
@@ -8,8 +8,8 @@ use super::super::{
use crate::{
strutil::format_i64,
trezorhal::power_manager::{
- charging_disable, charging_enable, charging_state, is_charging_limited, is_ntc_connected,
- soc, ChargingState,
+ charging_disable, charging_enable, charging_state, is_battery_connected,
+ is_charging_limited, is_ntc_connected, soc, ChargingState,
},
ui::{
component::{Component, Event, EventCtx, Never, Qr},
@@ -64,6 +64,8 @@ impl Welcome {
(true, "NTC Error")
} else if is_charging_limited() {
(true, "Charging Limited")
+ } else if !is_battery_connected() {
+ (true, "Battery Disconnected")
} else {
(false, "Error")
};
@@ -137,8 +139,15 @@ impl Component for Welcome {
// Error case: Battery OCV jump detected event
if e.battery_ocv_jump_detected {
self.error_active = true;
- charging_disable();
self.error_headline = "Battery OCV Jump";
+ charging_disable();
+ ctx.request_paint();
+ }
+ // Error case: Battery disconnection detected
+ if e.battery_connected_changed && !is_battery_connected() {
+ self.error_active = true;
+ self.error_headline = "Battery Disconnected";
+ charging_disable();
ctx.request_paint();
}
}
diff --git a/core/embed/upymod/modtrezorio/modtrezorio-pm.h b/core/embed/upymod/modtrezorio/modtrezorio-pm.h
index dfb06cb09..8864029ef 100644
--- a/core/embed/upymod/modtrezorio/modtrezorio-pm.h
+++ b/core/embed/upymod/modtrezorio/modtrezorio-pm.h
@@ -147,9 +147,12 @@ STATIC const mp_rom_map_elem_t mod_trezorio_pm_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_EVENT_WIRELESS_CONNECTED_CHANGED), MP_ROM_INT(1 << 3)},
{MP_ROM_QSTR(MP_QSTR_EVENT_NTC_CONNECTED_CHANGED), MP_ROM_INT(1 << 4)},
{MP_ROM_QSTR(MP_QSTR_EVENT_CHARGING_LIMITED_CHANGED), MP_ROM_INT(1 << 5)},
- {MP_ROM_QSTR(MP_QSTR_EVENT_BATTERY_OCV_JUMP_DETECTED), MP_ROM_INT(1 << 6)},
- {MP_ROM_QSTR(MP_QSTR_EVENT_BATTERY_TEMP_JUMP_UPDATED), MP_ROM_INT(1 << 7)},
- {MP_ROM_QSTR(MP_QSTR_EVENT_SOC_UPDATED), MP_ROM_INT(1 << 8)},
+ {MP_ROM_QSTR(MP_QSTR_EVENT_TEMP_CONTROL_ACTIVE_CHANGED),
+ MP_ROM_INT(1 << 6)},
+ {MP_ROM_QSTR(MP_QSTR_EVENT_BATTERY_CONNECTED_CHANGED), MP_ROM_INT(1 << 7)},
+ {MP_ROM_QSTR(MP_QSTR_EVENT_BATTERY_OCV_JUMP_DETECTED), MP_ROM_INT(1 << 8)},
+ {MP_ROM_QSTR(MP_QSTR_EVENT_BATTERY_TEMP_JUMP_UPDATED), MP_ROM_INT(1 << 9)},
+ {MP_ROM_QSTR(MP_QSTR_EVENT_SOC_UPDATED), MP_ROM_INT(1 << 10)},
};
STATIC MP_DEFINE_CONST_DICT(mod_trezorio_pm_globals,
mod_trezorio_pm_globals_table);
Why this scored 18/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.