feat(core): wakeup device from suspend on USB message.
What changed, and why it matters
This commit changes how the Trezor hardware wallet enters and wakes from its low-power suspend state. Previously, the device would fully shut down USB communication before entering suspend, so a connected computer could not wake it by sending a message. The change keeps USB active while the device is merely 'idling' with a cable connected, and only fully suspends USB after the cable is disconnected or a real wake-up event occurs. It also fixes a related LED effect bug so the RGB light is stopped when resuming. The overall intent is a feature improvement (wake on USB message), but it slightly increases the time window during which USB is live while the rest of the device is partially suspended, which could matter if the USB stack has flaws.
Treat as a normal feature commit, but review the USB driver resume path and suspend-phase2 boundary for race conditions or unintended USB traffic handling while the display/touch/haptic drivers are suspended. Verify that USB packets received during the partial-suspend loop cannot trigger operations that assume fully resumed drivers. No immediate patch or incident response is indicated by the diff alone.
Security signals we found
USB remains enabled during a partial suspend window instead of being stopped immediately
New pm_usb_is_connected() state is read under an IRQ lock to avoid races
LED effect is explicitly stopped on resume, fixing a possible stale-effect issue
Code comment now refers to 'USB connected' rather than only 'charging' as the suspend-loop condition
No explicit security advisory, CVE, or changelog entry present in the commit
Evidence from the diff
The patch splits suspend_drivers() into phase1 and phase2. Phase1 deinitializes display, touch, haptic, etc., but no longer stops USB. A new pm_usb_is_connected() helper is used to keep the CPU in a WFI loop while USB is connected, allowing USB interrupts to wake the system. Only when USB is disconnected and no other wakeup flags are set does phase2 call usb_stop() and rgb_led_suspend(). The unix emulator stub returns true for USB connected. Additionally, rgb_led_resume() now calls rgb_led_effect_stop() before restarting an ongoing effect, preventing stale LED state.
Changed components
core/embed/sys/suspend/stm32u5/suspend.ccore/embed/sys/suspend/stm32u5/suspend_io.ccore/embed/sys/suspend/inc/sys/suspend_io.hcore/embed/sys/power_manager/stm32u5/power_manager.ccore/embed/sys/power_manager/inc/sys/power_manager.hcore/embed/sys/power_manager/unix/power_manager.ccore/embed/io/rgb_led/stm32u5/rgb_led_lp.cInspect captured patch +61 / −24
diff --git a/core/embed/io/rgb_led/stm32u5/rgb_led_lp.c b/core/embed/io/rgb_led/stm32u5/rgb_led_lp.c
index bd95a4bbb..d4f1e755b 100644
--- a/core/embed/io/rgb_led/stm32u5/rgb_led_lp.c
+++ b/core/embed/io/rgb_led/stm32u5/rgb_led_lp.c
@@ -382,6 +382,8 @@ void rgb_led_suspend() { rgb_led_deinit(); }
void rgb_led_resume(const rgb_led_wakeup_params_t* params) {
rgb_led_init();
+ rgb_led_effect_stop();
+
if (params->ongoing_effect) {
rgb_led_effect_start(params->effect_type, 0);
}
diff --git a/core/embed/sys/power_manager/inc/sys/power_manager.h b/core/embed/sys/power_manager/inc/sys/power_manager.h
index d81e5c96c..c786b4795 100644
--- a/core/embed/sys/power_manager/inc/sys/power_manager.h
+++ b/core/embed/sys/power_manager/inc/sys/power_manager.h
@@ -216,6 +216,13 @@ pm_status_t pm_set_soc_target(uint8_t target);
*/
bool pm_is_charging(void);
+/**
+ * @brief Check if the USB is connected
+ *
+ * @return true if USB is connected, false otherwise
+ */
+bool pm_usb_is_connected(void);
+
/**
* @brief Suspends driver activity so the CPU can enter low-power mode.
*
diff --git a/core/embed/sys/power_manager/stm32u5/power_manager.c b/core/embed/sys/power_manager/stm32u5/power_manager.c
index 239d3c702..9b69f415c 100644
--- a/core/embed/sys/power_manager/stm32u5/power_manager.c
+++ b/core/embed/sys/power_manager/stm32u5/power_manager.c
@@ -625,6 +625,21 @@ bool pm_is_charging(void) {
return is_charging;
}
+bool pm_usb_is_connected(void) {
+ pm_driver_t* drv = &g_pm;
+
+ if (!drv->initialized) {
+ return false;
+ }
+
+ bool usb_connected;
+ irq_key_t irq_key = irq_lock();
+ usb_connected = drv->usb_connected;
+ irq_unlock(irq_key);
+
+ return usb_connected;
+}
+
bool pm_driver_resume(void) {
pm_driver_t* drv = &g_pm;
diff --git a/core/embed/sys/power_manager/unix/power_manager.c b/core/embed/sys/power_manager/unix/power_manager.c
index e9c61203f..96c236c1d 100644
--- a/core/embed/sys/power_manager/unix/power_manager.c
+++ b/core/embed/sys/power_manager/unix/power_manager.c
@@ -74,4 +74,6 @@ pm_status_t pm_get_state(pm_state_t* state) {
bool pm_is_charging(void) { return false; }
+bool pm_usb_connected(void) { return true; }
+
pm_status_t pm_set_soc_target(uint8_t target) { return PM_OK; }
diff --git a/core/embed/sys/suspend/inc/sys/suspend_io.h b/core/embed/sys/suspend/inc/sys/suspend_io.h
index 76286a991..b5f962eba 100644
--- a/core/embed/sys/suspend/inc/sys/suspend_io.h
+++ b/core/embed/sys/suspend/inc/sys/suspend_io.h
@@ -63,7 +63,15 @@ typedef struct {
* @param wakeup_params Pointer to a structure that will be filled with
* the state of the drivers before entering low-power mode.
*/
-void suspend_drivers(power_save_wakeup_params_t *wakeup_params);
+void suspend_drivers_phase1(power_save_wakeup_params_t *wakeup_params);
+
+/**
+ * @brief Suspends additional I/O drivers.
+ *
+ * This function is called after the device enters a low-power state.
+ * It suspends additional I/O drivers that were not suspended in phase 1.
+ */
+void suspend_drivers_phase2(void);
/**
* @brief Resumes I/O drivers.
diff --git a/core/embed/sys/suspend/stm32u5/suspend.c b/core/embed/sys/suspend/stm32u5/suspend.c
index 763ad6869..7165c8049 100644
--- a/core/embed/sys/suspend/stm32u5/suspend.c
+++ b/core/embed/sys/suspend/stm32u5/suspend.c
@@ -64,48 +64,44 @@ wakeup_flags_t system_suspend(void) {
power_save_wakeup_params_t wakeup_params = {0};
- // Deinitialize all drivers that are not required in low-power mode
- // (e.g., USB, display, touch, haptic, etc.).
- suspend_drivers(&wakeup_params);
+ // Deinitialize drivers that are not required in low-power charging phase
+ // (e.g., display, touch, haptic, etc.).
+ suspend_drivers_phase1(&wakeup_params);
wakeup_flags_t wakeup_flags = 0;
wakeup_flags_get(&wakeup_flags);
- // If the device is requested to go in suspend, but the battery is charging,
- // Keep in this loop until the the external power got diconnected or the
- // device is woke up. This state is signaled with RGB LED charging effect
- bool charging_in_suspend;
+ // If the device is requested to go in suspend, but the USB is connected,
+ // Keep in this loop until the external power got disconnected or the
+ // device is waked up. Also, if the battery is charging, the state is signaled
+ // with RGB LED charging effect.
do {
- if (pm_is_charging()) {
- charging_in_suspend = true;
-
#ifdef USE_RGB_LED
+ if (pm_is_charging()) {
if (!rgb_led_effect_ongoing()) {
rgb_led_effect_start(RGB_LED_EFFECT_CHARGING, 0);
}
-#endif
-
} else {
- charging_in_suspend = false;
+ rgb_led_effect_stop();
}
+#endif
__WFI();
wakeup_flags_get(&wakeup_flags);
- } while (charging_in_suspend && (wakeup_flags == 0));
+ } while (pm_usb_is_connected() && (wakeup_flags == 0));
-#ifdef USE_RGB_LED
- rgb_led_suspend();
-#endif
+ if (wakeup_flags == 0) {
+ // Deinitialize rest of the drivers before entering low-power mode
+ suspend_drivers_phase2();
+ }
// In the following loop, the system will attempt to enter low-power mode.
// Low-power mode may be exited for various reasons, but the loop will
// terminate only if a wakeup flag is set, indicating that user interaction
// is required or the user needs to be notified.
- wakeup_flags_get(&wakeup_flags);
-
while (wakeup_flags == 0) {
// Notify state machines running in the interrupt context about the
// impending low-power mode. They should complete any pending operations
diff --git a/core/embed/sys/suspend/stm32u5/suspend_io.c b/core/embed/sys/suspend/stm32u5/suspend_io.c
index eccea633a..103340a71 100644
--- a/core/embed/sys/suspend/stm32u5/suspend_io.c
+++ b/core/embed/sys/suspend/stm32u5/suspend_io.c
@@ -115,12 +115,9 @@ void resume_secure_drivers() {
#endif // SECURE_MODE
-void suspend_drivers(power_save_wakeup_params_t *wakeup_params) {
+void suspend_drivers_phase1(power_save_wakeup_params_t *wakeup_params) {
suspend_secure_drivers();
-#ifdef USE_USB
- usb_stop();
-#endif
#ifdef USE_HAPTIC
haptic_deinit();
#endif
@@ -141,6 +138,16 @@ void suspend_drivers(power_save_wakeup_params_t *wakeup_params) {
#endif
}
+void suspend_drivers_phase2(void) {
+#ifdef USE_USB
+ usb_stop();
+#endif
+
+#ifdef USE_RGB_LED
+ rgb_led_suspend();
+#endif
+}
+
void resume_drivers(const power_save_wakeup_params_t *wakeup_params) {
#ifdef USE_DISPLAY
// Reinitialize all drivers that were stopped earlier
Why this scored 35/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.