From de9b31afcbfc40fdf4d62bd44a08f63a70430363 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Sat, 4 Jul 2026 04:05:24 +0300 Subject: [PATCH 01/12] Add warm SoC reset and two-stage watchdog escalation (WB85 only) Warm reset pulses the PMIC RESET (PWROK) line for 100 ms instead of cycling the 5V rail. On WB 8.5 the line is wired to both the AXP15060 PWROK pin and the T507 RESET pin; with PMIC restart-on-PWROK-low disabled (AXP REG32[4]=0, the power-on default) the pulse resets only the SoC while all PMIC rails, including DRAM power, stay up. DRAM contents survive, so ramoops panic logs can be read back after reboot. Validated on WB 8.5.1: 16 MB test pattern survived EC-driven PWROK reset bit-perfect. The feature is gated by WBEC_HAS_WARM_RESET, defined only in config_wb85.h: the pulse behavior is hardware-validated on WB 8.5 only, so WB74 keeps its exact previous behavior (immediate hard power cycle on watchdog timeout, 2 s PMIC RESET hold on the reset_pmic request). On WB85 the watchdog escalates in two stages: the first timeout triggers a warm reset (new appended poweron reason 8, REASON_WATCHDOG_WARM); if Linux does not feed the watchdog via regmap before the next timeout, the EC falls back to the original hard power cycle (REASON_WATCHDOG). Feeding the watchdog re-arms the warm stage; a timeout-only regmap write restarts the timer but deliberately does not re-arm it. On WB85 the reset_pmic request from Linux (POWER_CTRL bit 2) now performs the short-pulse warm reset instead of the 2 s hold. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01RiQPqJiM3ftEk3SDda31Kf --- debian/changelog | 15 +++ include/config.h | 5 + include/config_wb85.h | 7 ++ include/linux-power-control.h | 1 + include/wdt.h | 1 + src/linux-power-control.c | 28 ++++++ src/wbec.c | 46 +++++++++ src/wdt.c | 13 +++ .../linux_power_control_test.c | 72 ++++++++++++++ unittests/regmap_test | Bin 0 -> 17280 bytes unittests/utest_helpers/wdt/utest_wdt.c | 13 +++ unittests/utest_helpers/wdt/utest_wdt.h | 1 + unittests/wbec/wbec_test.c | 92 +++++++++++++++++- unittests/wbec/wbec_test_stubs.c | 16 +++ unittests/wbec/wbec_test_stubs.h | 2 + unittests/wdt/wdt_test.c | 48 ++++++++- 16 files changed, 357 insertions(+), 3 deletions(-) create mode 100755 unittests/regmap_test diff --git a/debian/changelog b/debian/changelog index 31147c7..dc04f73 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,18 @@ +wb-ec-firmware (2.4.0) stable; urgency=medium + + * WB85: warm SoC reset via short PWROK/RESET pulse — PMIC rails and DRAM + stay powered, so pstore/ramoops panic logs survive the reboot + * WB85: two-stage watchdog — first timeout does a warm reset + (new poweron reason 8 "Watchdog (warm reset)"); if Linux does not feed + the watchdog before the next timeout, a hard power cycle follows + (reason 5 "Watchdog"). Consumers detecting watchdog resets must treat + both 5 and 8 as watchdog + * WB85: reset_pmic request (POWER_CTRL bit 2) now performs the short warm + pulse instead of the 2 s PMIC RESET hold + * WB74: behavior unchanged (no warm reset support) + + -- Wiren Board Robot Fri, 04 Jul 2026 14:00:00 +0300 + wb-ec-firmware (2.3.0) stable; urgency=medium * Port for Debian 13 diff --git a/include/config.h b/include/config.h index 3f31b51..56ff81a 100644 --- a/include/config.h +++ b/include/config.h @@ -47,6 +47,11 @@ // Время, на которое выключается питание при перезагрузке #define WBEC_POWER_RESET_TIME_MS 1000 +// Длительность импульса на линии PMIC RESET (PWROK) при тёплом сбросе. +// Тёплый сброс перезапускает SoC через его линию RESET, не отключая +// питание PMIC — содержимое DRAM (в т.ч. ramoops) сохраняется. +// Работает только если в PMIC не включен рестарт по PWROK (AXP REG32[4]=0) +#define WBEC_WARM_RESET_PULSE_MS 100 // Время загрузки Linux и драйверов WBEC // До этого времени питание выключается сразу при коротком нажатии // После - отправляется запрос в Linux diff --git a/include/config_wb85.h b/include/config_wb85.h index a95c404..2674950 100644 --- a/include/config_wb85.h +++ b/include/config_wb85.h @@ -100,6 +100,13 @@ // Поддержка spi-uart #define EC_UART_REGMAP_SUPPORT +// Тёплый сброс SoC импульсом на линии PWROK/RESET (см. linux_cpu_pwr_seq_warm_reset). +// Проверено на WB 8.5: линия заведена на RESET процессора T507, PMIC (AXP15060) +// игнорирует импульс при REG32[4]=0 и не отключает питание DRAM. +// На WB74 не включаем: поведение импульса на этой платформе не проверялось, +// там watchdog по-прежнему сразу делает жёсткий сброс по питанию +#define WBEC_HAS_WARM_RESET + // name freq sda scl #define SOFTWARE_I2C_DESC(macro) macro(WBMZ6, 100000, GPIOF, 1, GPIOF, 0) diff --git a/include/linux-power-control.h b/include/linux-power-control.h index a8013e9..f2f1f22 100644 --- a/include/linux-power-control.h +++ b/include/linux-power-control.h @@ -8,5 +8,6 @@ void linux_cpu_pwr_seq_on(void); void linux_cpu_pwr_seq_hard_off(void); void linux_cpu_pwr_seq_hard_reset(void); void linux_cpu_pwr_seq_reset_pmic(void); +void linux_cpu_pwr_seq_warm_reset(void); bool linux_cpu_pwr_seq_is_busy(void); void linux_cpu_pwr_seq_do_periodic_work(void); diff --git a/include/wdt.h b/include/wdt.h index 623f3fd..a46f3fd 100644 --- a/include/wdt.h +++ b/include/wdt.h @@ -6,4 +6,5 @@ void wdt_set_timeout(uint16_t secs); void wdt_start_reset(void); void wdt_stop(void); bool wdt_handle_timed_out(void); +bool wdt_handle_fed(void); void wdt_do_periodic_work(void); diff --git a/src/linux-power-control.c b/src/linux-power-control.c index 0bf3064..a18124c 100644 --- a/src/linux-power-control.c +++ b/src/linux-power-control.c @@ -31,6 +31,7 @@ enum pwr_state { PS_RESET_5V_WAIT, // Нужно при перезаргрузке - выключаем 5В и ждём разрядку линий PS_RESET_PMIC_WAIT, // Сброс PMIC через PMIC_RESET_PWROK. Ждём, пока пропадёт 3.3В + PS_WARM_RESET_PULSE, // Тёплый сброс SoC: короткий импульс на PMIC_RESET_PWROK }; struct pwr_ctx { @@ -173,6 +174,22 @@ void linux_cpu_pwr_seq_reset_pmic(void) new_state(PS_RESET_PMIC_WAIT); } +/** + * @brief Тёплый сброс SoC коротким импульсом на линии PMIC_RESET_PWROK. + * Линия одновременно заведена на PWROK PMIC и RESET процессора T507. + * Если в PMIC отключен рестарт по PWROK (AXP REG32[4]=0, значение по + * умолчанию), PMIC игнорирует импульс и все его выходы, включая питание + * DRAM, остаются включёнными — сбрасывается только SoC, содержимое DRAM + * сохраняется (это позволяет ramoops пережить сброс). + * Если же PMIC настроен на рестарт по PWROK, пропадёт 3.3В и штатная + * логика включения (PS_ON_STEP1_WAIT_3V3) выполнит полный цикл включения. + */ +void linux_cpu_pwr_seq_warm_reset(void) +{ + pmic_reset_gpio_on(); + new_state(PS_WARM_RESET_PULSE); +} + /** * @brief Статус работы алгоритма управления питанием * @@ -294,6 +311,17 @@ void linux_cpu_pwr_seq_do_periodic_work(void) } break; + // Тёплый сброс SoC: отпускаем линию RESET после короткого импульса. + // Если PMIC проигнорировал импульс, 3.3В на месте и включение + // завершится сразу; если PMIC перезапустился - штатное включение + case PS_WARM_RESET_PULSE: + if (in_state_time_ms() > WBEC_WARM_RESET_PULSE_MS) { + pmic_reset_gpio_off(); + pmic_pwron_gpio_off(); + new_state(PS_ON_STEP1_WAIT_3V3); + } + break; + // Сброс PMIC через RESET самого PMIC case PS_RESET_PMIC_WAIT: if ((!vmon_get_ch_status(VMON_CHANNEL_V33)) || (in_state_time_ms() > 2000)) { diff --git a/src/wbec.c b/src/wbec.c index 0f70c07..fa95449 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -35,6 +35,8 @@ m(REASON_WATCHDOG, "Watchdog" ) \ m(REASON_PMIC_OFF, "PMIC is unexpectedly off" ) \ m(REASON_UNKNOWN, "Unknown" ) \ + /* Новые значения добавлять только в конец: значения - часть ABI regmap */ \ + m(REASON_WATCHDOG_WARM, "Watchdog (warm reset)" ) \ #define __LINUX_POWERON_REASON_NAME(name, string) name, #define __LINUX_POWERON_REASON_STRING(name, string) string, @@ -77,6 +79,11 @@ struct wbec_ctx { unsigned power_loss_cnt; systime_t power_loss_timestamp; enum linux_poweron_reason poweron_reason; + // Была ли уже попытка тёплого сброса по watchdog. + // Сбрасывается, когда Linux сбрасывает watchdog (система жива). + // Если после тёплого сброса Linux так и не начал сбрасывать watchdog, + // следующий таймаут приведёт к жёсткому сбросу по питанию + bool wd_warm_reset_attempted; }; static struct wbec_ctx wbec_ctx; @@ -546,13 +553,51 @@ void wbec_do_periodic_work(void) linux_cpu_pwr_seq_hard_reset(); new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); } else if (linux_powerctrl_req == LINUX_POWERCTRL_PMIC_RESET) { +#if defined(WBEC_HAS_WARM_RESET) + // Тёплый сброс SoC по запросу из Linux: короткий импульс на линии + // PWROK/RESET. Питание PMIC (и DRAM) не отключается, поэтому + // сохранённые в DRAM логи (ramoops) переживают перезагрузку + wbec_ctx.poweron_reason = REASON_REBOOT; + console_print("\r\n\n"); + console_print_w_prefix("Warm reset request, pulse PMIC RESET (PWROK) line now\r\n\n"); + linux_cpu_pwr_seq_warm_reset(); + new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); +#else + // Историческое поведение: сброс PMIC через линию RESET (до 2 с) wbec_ctx.poweron_reason = REASON_REBOOT; console_print("\r\n\n"); console_print_w_prefix("PMIC reset request, activate PMIC RESET line now\r\n\n"); linux_cpu_pwr_seq_reset_pmic(); new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); +#endif } +#if defined(WBEC_HAS_WARM_RESET) + // Если Linux сбрасывает watchdog - система жива, + // разрешаем следующую попытку тёплого сброса + if (wdt_handle_fed()) { + wbec_ctx.wd_warm_reset_attempted = false; + } + + // Если сработал WDT - сначала пробуем тёплый сброс (DRAM сохраняется, + // логи ramoops можно будет прочитать после перезагрузки). + // Если после тёплого сброса система не ожила (Linux не сбросил + // watchdog до следующего таймаута) - жёсткий сброс по питанию + if (wdt_handle_timed_out()) { + console_print("\r\n\n"); + if (!wbec_ctx.wd_warm_reset_attempted) { + wbec_ctx.wd_warm_reset_attempted = true; + wbec_ctx.poweron_reason = REASON_WATCHDOG_WARM; + console_print_w_prefix("Watchdog is timed out, try warm reset first (DRAM is preserved).\r\n"); + linux_cpu_pwr_seq_warm_reset(); + } else { + wbec_ctx.poweron_reason = REASON_WATCHDOG; + console_print_w_prefix("Watchdog is timed out again, reset power.\r\n"); + linux_cpu_pwr_seq_hard_reset(); + } + new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); + } +#else // Если сработал WDT - перезагружаемся по питанию if (wdt_handle_timed_out()) { wbec_ctx.poweron_reason = REASON_WATCHDOG; @@ -561,6 +606,7 @@ void wbec_do_periodic_work(void) linux_cpu_pwr_seq_hard_reset(); new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); } +#endif // Если пропало 3.3В - пробуем перезапустить питание, но не более N раз за M минут // Если питание пропадает слишком часто - выключаемся diff --git a/src/wdt.c b/src/wdt.c index c128d49..3b7162f 100644 --- a/src/wdt.c +++ b/src/wdt.c @@ -20,6 +20,7 @@ struct wdt_ctx { systime_t timestamp; bool run; bool timed_out; + bool fed; }; static struct wdt_ctx wdt_ctx = { @@ -56,6 +57,17 @@ bool wdt_handle_timed_out(void) return ret; } +// Возвращает true, если Linux сбрасывал watchdog через regmap +// с момента предыдущего вызова. Признак того, что система жива. +bool wdt_handle_fed(void) +{ + bool ret = wdt_ctx.fed; + if (ret) { + wdt_ctx.fed = 0; + } + return ret; +} + void wdt_do_periodic_work(void) { if (wdt_ctx.run) { @@ -78,6 +90,7 @@ void wdt_do_periodic_work(void) } if (w.reset) { wdt_start_reset(); + wdt_ctx.fed = 1; } } diff --git a/unittests/linux-power-control/linux_power_control_test.c b/unittests/linux-power-control/linux_power_control_test.c index a769348..d880058 100644 --- a/unittests/linux-power-control/linux_power_control_test.c +++ b/unittests/linux-power-control/linux_power_control_test.c @@ -699,6 +699,76 @@ static void test_periodic_reset_pmic_completes_when_v33_is_lost(void) ); } +// Сценарий: тёплый сброс - короткий импульс на PMIC RESET (PWROK), 5В не отключается. +// До действия: питание включено, 3.3В есть. +// После действия: RESET удерживается WBEC_WARM_RESET_PULSE_MS, затем отпускается, +// при живом 3.3В последовательность сразу завершается (PMIC проигнорировал импульс). +static void test_periodic_warm_reset_pulses_reset_line_and_completes(void) +{ + linux_cpu_pwr_seq_init(true); + prepare_periodic_runtime(true, true); + linux_cpu_pwr_seq_do_periodic_work(); + TEST_ASSERT_FALSE_MESSAGE(linux_cpu_pwr_seq_is_busy(), "Power sequence must be idle before warm reset"); + + linux_cpu_pwr_seq_warm_reset(); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 1, utest_gpio_get_output_state(pmic_reset_gpio), "PMIC RESET GPIO must be asserted right after warm reset" + ); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 1, utest_gpio_get_output_state(linux_power_gpio), "Linux power (5V) GPIO must stay high during warm reset" + ); + TEST_ASSERT_TRUE_MESSAGE(linux_cpu_pwr_seq_is_busy(), "Power sequence must be busy during warm reset pulse"); + + // До истечения импульса линия удерживается + utest_systick_advance_time_ms(WBEC_WARM_RESET_PULSE_MS); + linux_cpu_pwr_seq_do_periodic_work(); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 1, utest_gpio_get_output_state(pmic_reset_gpio), "PMIC RESET GPIO must stay high at exact pulse length" + ); + + // После истечения импульса линия отпущена, 5В осталось включено + utest_systick_advance_time_ms(1); + linux_cpu_pwr_seq_do_periodic_work(); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 0, utest_gpio_get_output_state(pmic_reset_gpio), "PMIC RESET GPIO must be low after pulse" + ); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 1, utest_gpio_get_output_state(linux_power_gpio), "Linux power (5V) GPIO must stay high after warm reset" + ); + + // 3.3В на месте → последовательность завершена + linux_cpu_pwr_seq_do_periodic_work(); + TEST_ASSERT_FALSE_MESSAGE(linux_cpu_pwr_seq_is_busy(), "Power sequence must complete when 3.3V is alive"); +} + +// Сценарий: тёплый сброс, но PMIC перезапустился (3.3В пропало после импульса). +// После действия: штатная логика включения пробует PMIC PWRON после 1с ожидания 3.3В. +static void test_periodic_warm_reset_falls_back_to_power_on_when_v33_lost(void) +{ + linux_cpu_pwr_seq_init(true); + prepare_periodic_runtime(true, true); + linux_cpu_pwr_seq_do_periodic_work(); + + linux_cpu_pwr_seq_warm_reset(); + + // PMIC перезапустился: 3.3В пропало + utest_vmon_set_ch_status(VMON_CHANNEL_V33, false); + + utest_systick_advance_time_ms(WBEC_WARM_RESET_PULSE_MS + 1); + linux_cpu_pwr_seq_do_periodic_work(); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 0, utest_gpio_get_output_state(pmic_reset_gpio), "PMIC RESET GPIO must be low after pulse" + ); + TEST_ASSERT_TRUE_MESSAGE(linux_cpu_pwr_seq_is_busy(), "Power sequence must stay busy waiting for 3.3V"); + + // Через 1с без 3.3В - попытка включить PMIC через PWRON (штатная логика) + utest_systick_advance_time_ms(1001); + linux_cpu_pwr_seq_do_periodic_work(); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 1, utest_gpio_get_output_state(pmic_pwron_gpio), "PMIC PWRON GPIO must be asserted when 3.3V did not return" + ); +} + // Сценарий: вызов linux_cpu_pwr_seq_reset_pmic и сохранение 3.3V. // До действия: RESET отпущен. // После действия: по таймауту >2000мс RESET завершается даже при наличии 3.3V. @@ -978,6 +1048,8 @@ int main(void) RUN_TEST(test_periodic_off_complete_disables_stepup_when_enabled); RUN_TEST(test_periodic_v50_loss_goes_to_standby_with_saved_off_state); + RUN_TEST(test_periodic_warm_reset_pulses_reset_line_and_completes); + RUN_TEST(test_periodic_warm_reset_falls_back_to_power_on_when_v33_lost); RUN_TEST(test_periodic_reset_pmic_completes_when_v33_is_lost); RUN_TEST(test_periodic_reset_pmic_completes_by_timeout); RUN_TEST(test_periodic_reset_pmic_timeout_switches_state_only_after_2000ms); diff --git a/unittests/regmap_test b/unittests/regmap_test new file mode 100755 index 0000000000000000000000000000000000000000..98c165833f2010f4a88e69240c7c95a4aa483a79 GIT binary patch literal 17280 zcmeHOe{@vUoxd{>2!bT22!fS|ibX9$_#sDB%JAdiu;rJL)E_HOCdo{a(@7?sd4W** z5gqh69dX%H-FiH94{lE_t(3AH6xu`W2$V!O)nHMxYPT#~cPE?F6Dw+jklD}o-ur!d z^9%hlUXMp`8796isFf*Jh+i`@x9SRrU)(G% z#_tqyl^B8caEU3tUlDLs>G0;$LX8gtCB2DM8H8S`V8N6lBuaW0Dm@es3R9u`L3#zm z&}}#U_c6p2f+_QDe}-fW;iBCm70%SDN1^PRq4>KNiS}pPUgon9Q`@7IMl;bo0C!^XB>zG2d+I znBp?Swy8b}Uy@Pzcu?YmQ8^KQwNBLr|GgOSC4;bPfEk0}-v#h(zzuxe zwv0dTb&q;^B2l@N8IXf#$Sf`gzLo!4#UTTqn3Vo-cao}@Ol0| z@%R6L6b%>4G}bt&7DGKu+@aeCB;PC6p-qL?89u&ujZ^q;y^b(muJJ;@;xo?ej)e-R z@u4}u<&*<=&daO=m+P!n=yBlegIK)|oaZj#hcr&hBIktgj030RgiEUfr&zhPJ8)iN znKwj-GBA{Zp$rUVU?>Aa85qjI|K|*RSaj9dK(f0qkS_SwnL-2}&RE`DdmwqN@Tg2n z?xu$T=caALZ_yOLAo&{Nbe_)Ta*-XfO&5rruMf28!m#tmK$|Y)I``Y{+_WvQ7Acyt zR7iWdX+Br`I#>HDSNrn3_N+fGPo20wkUAYm{;_AxT60!r*2{tI3m!mNx$z%jr*bCZ zE1E((6LfIyWcy#?u2vOHZkUP2~FiK&mJ3 z()$YoFZB)!c#a27^jnv~z>l?CV zk?riy<+4@C&hdinXnWQ{*Y;2Pog2U`gHCF5U)t;oBr_8O+b^R&+&>nha&WkJ-BDuG zI4Y4;OSTO!^n zJuf=gp&B=-s6B9f0HK3zDrElAE!U99aX6Zezyqn4Lu4iUFXwZ)L;nb&&gs|}B+Ub! zwD~TAO_v>*ePu^@*TvgHa`>__OZn60;sy@NR$X)*UuxYAMA z18MURL@cEpNSUVsX?y|IKnv|`i+Naf?7Fmh7)hrrr_F4@bEN2?xhInH5<#aTDG>88 z;v{=Zl)d@p;Z3bT0_if?U6~D}UInulSxcISJju<6CKNsX0D7Tf`nBx(v)I_Qm>H^f z?#EB|4FIsd|G4=a7{`dw|%#XKopl%&m*@Ng1Q%1pIw znxv55+sM{JLdXC;2=rr`!SvEA6vZqw{SU5nG z*H4_nKedHNDq)_3d1-7z8r#%Zyo-sEZNetL#q5wt`)B;7%??B>RJ2jFeo3qs=?C~! zI`n!x=*U%=D!ZNTZ?xS%3R$;e7Ka(MRzd5ZoLV1!s(aW~=2m%LsZ55-_nj(7Y?Z5% zW(NkR1BVQ)9uRz531%NB8H)J=OBpR=--Ya#UQ<`*DgW=+)bkhRt*P=P(2Kd-hr#Ph zo|^JY45>)H^t;okc6AO&+1$uW*#gXC1ydx&sop@UF$?W8(56-XXUI>qu_y zp0NHRIk`J`AcnNr4KtIJnZSIr!x|}>UF#KR5jZrFRBM%L^&dCSiJ}(@eu}8n@w^nP z8Fjf&CglLdD39~65vEUa+Y3IPhF)wRS32uQMOq5l(2!312fWDRf>w=gNdH_aWzgxL<77o02KclMZUU$lpR+OmwJK~c zmKNHJURA@X7P~NCU%HjVxQPA4v36s7d=?C3O$Brs;AlWjAsn&jTMD>F6zbe zPOtpX^DR}o6u1W#N67YKu{h5mUCZ#77+iN!C+@5}Wt?@hv~+dpEk-Gx*Tw3MX;nsS zeK;NsH6)DsaFb=w3xdR=A$zjoNxVg$$ur5vgtaD_za{qzu8zSUld?V1=uUPz28{rIGMJ zzgNW&O0cpfR9_8|NHkP!B*K;viN%dl4#n9Yw=l(384ZWx;VLP{{cy{nzqVZtQ7no^ z4J(|m5=KKPkqB2!MO*{ruY3M^CJ(!n{>woBW65;qat0=6@t?3M0X0C&LCZl4L6^Uu z%Qb=4fIbEKUC=|IKLYIleI2wH^epH^?4W<}VJ_zfJq%h7`fs4kpr3;70=@1`E|&pa z1$qiJ3Q7-Elb{CZA3&FaF2MB-{qN%u&__W}gYE_`#pPQo=;%+dAp{-Ki#V~Sn%17MSqB7TNFWN~7QTmo`$4&ougCCx8UCga zxM*zgBid(bUWFO8gR0dKU|iHnvU0$iB=Q4^#Ap63YDc?Q@XyRg2z6x$ZH7Hc4)_px9h;T zUl}DX|0_#X_Ajqd&}KQ@(31T_UL}MZoU*+`+rwR8=1Yq`y1icW@jyZHxgS#K|L}+Z z_w6=WqA!B|ny%5bTvNCFUuqZnmBj5!mfT`YFRN&*w;GLEb9{4rB{SwV%I54Xv*-Fs z=K5w`uNj|bljiAg@j=clGYSCx=k+n2FhdJm3OblPjh7dbyT{cp(j(l^|6@f{mO1z( zoR1d`3%VxsV7426h467i|FZUo%kBBExI%0~h3xS0$M~aCe!P7=G5%vz28;iBDSwGD z3?-RSL>vJn`#;g~%RLIPS2f6NUgOO!e743j8uw~*w`u%{3ty)3V;Vl0UlX3vs%zAUvC124u|uGpW?TTb%cae~i-%w2r}I~5Wy77;x`n!tLk#@K>KxwZzjxuT}Y`A81N%>;2U+3o+8uwy7 z9W4LjffwWWZPmw-78eg|I~l#M%6l?kPSWy*pCuK&PTDUP_i88do&{JpYWxYEH;#es zE73SUt8uw!g?u@1%0sbV+2!9TaDy8T`iQo3R@-??^L_~YYKI(GZ=`;CaaH6xUx+&^ zSfDr$pPs9FF>SdEd0uTl)W&=Iuif|c}2M*@;nv0!yHRuPH@tE^Z&5ezjp ziON`ALo|#VslJlICFm`a+F&Ri4{Z#F>#g`k5s8QD!ojM>y1I=}akPSvwDLr8x3(_S z5Tv`I2us~DPCzUY(~V$)-d|CSAV{z>hEp{Ik=l5|BA9_F?q|z3Zp536Hg0V;hH+coVQh@9V zd2@K6S?%Ph*zDWS$kDRWMJvsqxoT+;uhA^MXVs#W%a;JgOHn~Hpt*sir6PFyiq(r3 ztq87Ow(L%GZE)?P#Vbr`xL^PA^Lssi;RPO*7da~OeIi?rUIGe+=_bFB!>eB)%HwNg zJXHub#KR4^6&{Q=gySI#4;T;Inf zI2ds}bRg?EB&=9%J-u>;SD|>s=p89?{h2qU92RKSg}vOji=&O5w9GAB2yV1W3o^K={Hn zdJ(9pf;Da_8)`LB8eE5~P+cvIDRm5&@L?IM!|KAz-j}Adta|XpvMI|%tgBR*@_taE zN|x(Hufdo0V61<$)@NEmLXILgS&sHR)Sdb@TA%5)4q2-5d5~ecPIBt=zJjU2f||PZ z?*fj;95Ifc_qj~jKkswh@rR&KdtBD<(D5_v(S9jL$}{WpezX=CsjxoptC{jX8X~fg z4QKf_8nj1cecqQc<^40q&vHz+K%e%!+~$2RQ@?iRj$gxKJ1Ue@)^FGQVx}1_$o}2^ z|Gw5=tmSxL&$Qg3PdRhzKLd<%!uIX+zCQo!%LZ-bv-4-COW)9XOxM_&s-oGpn;*OM z8?*pZmS;2EX1dR%->ex-{j8`d>oKM4KWG0ln!$9OeS=6DU_CbbYnMJ>FEhQE1vPaa zKflrXJpRJaf|}Cxp0jA!hJK40#n1Y@-(X75*qp`f{~a)$`fKz7%XFv9KhGbw`5r1` z@ntc7f0o}Dcgu5|=?Bm)a_IAYwB0iRqOhE54YV0Q4+hO$_Rsq;e*d4}^QYsF3hVJW zJ_n5Y&-#2HZP!ea7NT7K4*ak_(-Ba1>I=V?Em4&Hum#rRHa+7cjQtya#VOYMyIlij zD7O7OPW#v5HqQDwjW$lPQgKqAJBp)z{p?>a3||3^|90B> Date: Sat, 4 Jul 2026 21:58:32 +0300 Subject: [PATCH 02/12] Fix watchdog wedge: never start a second power sequence over one in flight A watchdog timeout racing a 3.3V loss (or a reset_pmic request) in the same main loop pass started a second power sequence on top of the warm reset pulse already in flight. The pulse never completed, the PMIC RESET (PWROK) line stayed latched and the SoC was held in reset forever: 5V rail on, no SPL/U-Boot output ever, and no further watchdog recovery (the escalation was latched to hard resets, which did not release the line). Matches the failure observed on a WB 8.5.1 bench unit after a rapid kernel-hang reboot loop. The same race latched the line on WB74 via the reset_pmic request path. - wbec: allow only one power action per pass in WORKING; once a power sequence is started, defer the watchdog and 3.3V checks to the next passes (pending watchdog flags are not lost) - wbec: restart the escalation after a stage-2 hard reset so unfed timeouts alternate warm/hard; the first hang of every fresh boot gets a DRAM-preserving warm reset and ramoops stays readable - linux-power-control: hard_reset/hard_off/power-on always release the PMIC RESET (PWROK) line so no aborted sequence can leave it latched; a completed warm pulse turns 5V on before the power-on sequence - add wbec + linux-power-control + wdt integration test suite with a board model (PMIC/SoC/5V rail): reproduces the wedge, sweeps PMIC failure timing across all reset phases, asserts bounded recovery Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EVPkc2g2yir6SGyrED6uNT --- debian/changelog | 18 + src/linux-power-control.c | 15 + src/wbec.c | 24 + src/wdt.c | 10 + .../linux_power_control_test.c | 86 +++ unittests/wbec-integration/Makefile | 64 ++ .../wbec-integration/wbec_integration_test.c | 572 ++++++++++++++++++ unittests/wbec/wbec_test.c | 136 +++++ 8 files changed, 925 insertions(+) create mode 100644 unittests/wbec-integration/Makefile create mode 100644 unittests/wbec-integration/wbec_integration_test.c diff --git a/debian/changelog b/debian/changelog index dc04f73..b46d29b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -10,6 +10,24 @@ wb-ec-firmware (2.4.0) stable; urgency=medium * WB85: reset_pmic request (POWER_CTRL bit 2) now performs the short warm pulse instead of the 2 s PMIC RESET hold * WB74: behavior unchanged (no warm reset support) + * Fix state-machine wedge: a watchdog timeout racing a 3.3V loss or a + reset_pmic request in the same main loop pass started a second power + sequence on top of the warm reset (or PMIC reset) already in flight, + aborting the pulse and leaving the PMIC RESET (PWROK) line latched - + the SoC stayed held in reset forever (5V on, no SPL/U-Boot output, + watchdog resets no longer able to recover the board). Now only one + power action is taken per pass, hard_reset/hard_off/power-on always + release the reset line, and a completed warm pulse guarantees 5V is on + * Watchdog escalation restarts after each hard power cycle: with no + feeds the resets alternate warm/hard instead of latching to hard-only, + so the first hang of every fresh boot still gets a DRAM-preserving + warm reset (ramoops stays readable) + * Poweroff request racing a watchdog timeout no longer resurrects the + board: the power-off wins, the deferred timeout is absorbed + * Add wbec + linux-power-control + wdt integration test suite with a + board model (PMIC/SoC/5V rail simulation): reproduces the wedge, + sweeps PMIC failure timing across all reset sequence phases and + asserts the watchdog always recovers the SoC in bounded time -- Wiren Board Robot Fri, 04 Jul 2026 14:00:00 +0300 diff --git a/src/linux-power-control.c b/src/linux-power-control.c index a18124c..7e4b847 100644 --- a/src/linux-power-control.c +++ b/src/linux-power-control.c @@ -137,6 +137,10 @@ void linux_cpu_pwr_seq_on(void) return; } + // Линия PMIC RESET (PWROK) не должна достаться последовательности + // включения взведённой (например, от прерванного тёплого сброса) - + // иначе SoC останется заклиненным в сбросе + pmic_reset_gpio_off(); linux_cpu_pwr_5v_gpio_on(); new_state(PS_ON_STEP1_WAIT_3V3); } @@ -149,6 +153,9 @@ void linux_cpu_pwr_seq_hard_off(void) { linux_cpu_pwr_5v_gpio_off(); pmic_pwron_gpio_off(); + // Отпускаем линию сброса: никакая последовательность не должна + // оставлять её взведённой после своего завершения или прерывания + pmic_reset_gpio_off(); new_state(PS_OFF_COMPLETE); } @@ -160,6 +167,10 @@ void linux_cpu_pwr_seq_hard_reset() { linux_cpu_pwr_5v_gpio_off(); pmic_pwron_gpio_off(); + // Отпускаем линию сброса: если жёсткий сброс прервал тёплый сброс + // или сброс PMIC, линия не должна остаться взведённой - иначе после + // подачи 5В SoC навсегда останется в сбросе + pmic_reset_gpio_off(); new_state(PS_RESET_5V_WAIT); } @@ -318,6 +329,10 @@ void linux_cpu_pwr_seq_do_periodic_work(void) if (in_state_time_ms() > WBEC_WARM_RESET_PULSE_MS) { pmic_reset_gpio_off(); pmic_pwron_gpio_off(); + // Тёплый сброс всегда заканчивается последовательностью включения. + // Гарантируем ей 5В: если тёплый сброс был запрошен, когда 5В + // оказалось снятым, ожидание 3.3В без 5В бессмысленно + linux_cpu_pwr_5v_gpio_on(); new_state(PS_ON_STEP1_WAIT_3V3); } break; diff --git a/src/wbec.c b/src/wbec.c index fa95449..3b89dc0 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -572,6 +572,18 @@ void wbec_do_periodic_work(void) #endif } + // За один проход разрешено только одно действие с питанием. + // Если запрос из Linux уже начал смену состояния, проверки ниже + // (watchdog, контроль 3.3В) выполнять нельзя: их обработчики + // запускают свои последовательности поверх уже начатой и могут + // прервать её (например, оставить линию PMIC RESET (PWROK) + // взведённой навсегда - SoC заклинит в сбросе). + // Взведённые флаги watchdog не теряются: они будут обработаны + // после завершения начатой последовательности + if (wbec_ctx.state != WBEC_STATE_WORKING) { + break; + } + #if defined(WBEC_HAS_WARM_RESET) // Если Linux сбрасывает watchdog - система жива, // разрешаем следующую попытку тёплого сброса @@ -594,6 +606,11 @@ void wbec_do_periodic_work(void) wbec_ctx.poweron_reason = REASON_WATCHDOG; console_print_w_prefix("Watchdog is timed out again, reset power.\r\n"); linux_cpu_pwr_seq_hard_reset(); + // Жёсткий сброс - последняя ступень эскалации. После него + // начинается новый цикл загрузки, и первое зависание в нём + // снова заслуживает тёплого сброса (с сохранением DRAM/ramoops), + // поэтому эскалация начинается заново + wbec_ctx.wd_warm_reset_attempted = false; } new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); } @@ -608,6 +625,13 @@ void wbec_do_periodic_work(void) } #endif + // Сработал watchdog - сброс уже начат. Контроль 3.3В в этом проходе + // пропускаем, чтобы не запустить вторую последовательность поверх + // начатой (см. комментарий выше) + if (wbec_ctx.state != WBEC_STATE_WORKING) { + break; + } + // Если пропало 3.3В - пробуем перезапустить питание, но не более N раз за M минут // Если питание пропадает слишком часто - выключаемся // Это происходит, например, при питании через плохой USB кабель. diff --git a/src/wdt.c b/src/wdt.c index 3b7162f..03796e9 100644 --- a/src/wdt.c +++ b/src/wdt.c @@ -68,6 +68,16 @@ bool wdt_handle_fed(void) return ret; } +#ifdef __unittest_env__ + #include + + void utest_wdt_module_reset_state(void) + { + memset(&wdt_ctx, 0, sizeof(wdt_ctx)); + wdt_ctx.timeout_s = WBEC_WATCHDOG_INITIAL_TIMEOUT_S; + } +#endif + void wdt_do_periodic_work(void) { if (wdt_ctx.run) { diff --git a/unittests/linux-power-control/linux_power_control_test.c b/unittests/linux-power-control/linux_power_control_test.c index d880058..5c4a942 100644 --- a/unittests/linux-power-control/linux_power_control_test.c +++ b/unittests/linux-power-control/linux_power_control_test.c @@ -977,6 +977,89 @@ static void test_periodic_long_press_waits_until_button_release(void) ); } +// Сценарий: жёсткий сброс прерывает тёплый сброс (гонка запросов). +// До действия: тёплый сброс начат, линия PMIC RESET (PWROK) взведена. +// После действия: hard_reset обязан отпустить линию сброса. Иначе ни одно +// состояние последовательности включения её не отпустит, и после подачи 5В +// SoC навсегда останется заклиненным в сбросе. +static void test_hard_reset_aborting_warm_reset_releases_reset_line(void) +{ + linux_cpu_pwr_seq_init(true); + prepare_periodic_runtime(true, true); + linux_cpu_pwr_seq_do_periodic_work(); + + linux_cpu_pwr_seq_warm_reset(); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 1, utest_gpio_get_output_state(pmic_reset_gpio), "PMIC RESET GPIO must be asserted during warm reset pulse" + ); + + linux_cpu_pwr_seq_hard_reset(); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 0, utest_gpio_get_output_state(pmic_reset_gpio), "hard_reset must release the PMIC RESET (PWROK) line" + ); + + // Последовательность после жёсткого сброса завершается штатно + utest_systick_advance_time_ms(WBEC_POWER_RESET_TIME_MS + 1); + linux_cpu_pwr_seq_do_periodic_work(); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 1, utest_gpio_get_output_state(linux_power_gpio), "Linux power GPIO must be re-enabled after hard reset" + ); + utest_vmon_set_ch_status(VMON_CHANNEL_V33, true); + linux_cpu_pwr_seq_do_periodic_work(); + TEST_ASSERT_FALSE_MESSAGE(linux_cpu_pwr_seq_is_busy(), "Power sequence must complete after aborted warm reset"); +} + +// Сценарий: выключение прерывает тёплый сброс. +// После действия: hard_off отпускает линию сброса (никакая последовательность +// не оставляет её взведённой). +static void test_hard_off_aborting_warm_reset_releases_reset_line(void) +{ + linux_cpu_pwr_seq_init(true); + prepare_periodic_runtime(true, true); + linux_cpu_pwr_seq_do_periodic_work(); + + linux_cpu_pwr_seq_warm_reset(); + linux_cpu_pwr_seq_hard_off(); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 0, utest_gpio_get_output_state(pmic_reset_gpio), "hard_off must release the PMIC RESET (PWROK) line" + ); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 0, utest_gpio_get_output_state(linux_power_gpio), "Linux power GPIO must be low after hard_off" + ); +} + +// Сценарий: тёплый сброс запрошен, когда 5В снято (например, поверх +// паузы жёсткого сброса). +// После действия: по завершении импульса последовательность включения +// получает 5В - ожидание 3.3В без 5В бессмысленно. +static void test_warm_reset_completion_forces_5v_on(void) +{ + linux_cpu_pwr_seq_init(true); + prepare_periodic_runtime(true, false); + + linux_cpu_pwr_seq_hard_reset(); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 0, utest_gpio_get_output_state(linux_power_gpio), "Linux power GPIO must be low during hard reset wait" + ); + + linux_cpu_pwr_seq_warm_reset(); + utest_systick_advance_time_ms(WBEC_WARM_RESET_PULSE_MS + 1); + linux_cpu_pwr_seq_do_periodic_work(); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 0, utest_gpio_get_output_state(pmic_reset_gpio), "PMIC RESET GPIO must be released after the pulse" + ); + TEST_ASSERT_EQUAL_UINT32_MESSAGE( + 1, utest_gpio_get_output_state(linux_power_gpio), "Warm reset completion must re-enable 5V for power-on" + ); + + utest_vmon_set_ch_status(VMON_CHANNEL_V33, true); + linux_cpu_pwr_seq_do_periodic_work(); + TEST_ASSERT_FALSE_MESSAGE(linux_cpu_pwr_seq_is_busy(), "Power sequence must complete after warm reset"); +} + // Сценарий: standalone вызов reset_pmic. // До действия: RESET-линия опущена. // После действия: RESET-линия поднимается сразу. @@ -1050,6 +1133,9 @@ int main(void) RUN_TEST(test_periodic_warm_reset_pulses_reset_line_and_completes); RUN_TEST(test_periodic_warm_reset_falls_back_to_power_on_when_v33_lost); + RUN_TEST(test_hard_reset_aborting_warm_reset_releases_reset_line); + RUN_TEST(test_hard_off_aborting_warm_reset_releases_reset_line); + RUN_TEST(test_warm_reset_completion_forces_5v_on); RUN_TEST(test_periodic_reset_pmic_completes_when_v33_is_lost); RUN_TEST(test_periodic_reset_pmic_completes_by_timeout); RUN_TEST(test_periodic_reset_pmic_timeout_switches_state_only_after_2000ms); diff --git a/unittests/wbec-integration/Makefile b/unittests/wbec-integration/Makefile new file mode 100644 index 0000000..8ebbfe2 --- /dev/null +++ b/unittests/wbec-integration/Makefile @@ -0,0 +1,64 @@ +# This test name +TEST_NAME = wbec_integration_test + +# Project root directory +PROJ_DIR = ../.. + +# Source files to be checked +# Интеграционный тест: реальные wbec + linux-power-control + wdt работают вместе, +# как в main loop прошивки. Проверяется взаимодействие автоматов состояний. +TESTED_SRC += $(PROJ_DIR)/src/wbec.c +TESTED_SRC += $(PROJ_DIR)/src/linux-power-control.c +TESTED_SRC += $(PROJ_DIR)/src/wdt.c + +# Unittest helpers directory +UTEST_HELPERS_DIR = ../utest_helpers + +# Auxiliary source files used in tests +AUX_SRC += $(UTEST_HELPERS_DIR)/adc/utest_adc.c +AUX_SRC += $(UTEST_HELPERS_DIR)/systick/utest_systick.c +AUX_SRC += $(UTEST_HELPERS_DIR)/voltage-monitor/utest_voltage_monitor.c +AUX_SRC += $(UTEST_HELPERS_DIR)/mcu-pwr/utest_mcu_pwr.c +AUX_SRC += $(UTEST_HELPERS_DIR)/regmap/utest_regmap.c +AUX_SRC += $(UTEST_HELPERS_DIR)/system-led/utest_system_led.c +AUX_SRC += $(UTEST_HELPERS_DIR)/wbmcu_system/utest_wbmcu_system.c +AUX_SRC += $(UTEST_HELPERS_DIR)/wdt-stm32/utest_wdt_stm32.c +AUX_SRC += $(UTEST_HELPERS_DIR)/wbmz-common/utest_wbmz_common.c +AUX_SRC += $(UTEST_HELPERS_DIR)/rtc/utest_rtc.c +AUX_SRC += $(UTEST_HELPERS_DIR)/irq/utest_irq.c +AUX_SRC += $(UTEST_HELPERS_DIR)/pwrkey/utest_pwrkey.c +AUX_SRC += $(UTEST_HELPERS_DIR)/console/utest_console.c +AUX_SRC += $(UTEST_HELPERS_DIR)/gpio/utest_gpio.c + +# Include directories +INC += . +INC += $(UTEST_HELPERS_DIR) +INC += $(UTEST_HELPERS_DIR)/adc +INC += $(UTEST_HELPERS_DIR)/systick +INC += $(UTEST_HELPERS_DIR)/voltage-monitor +INC += $(UTEST_HELPERS_DIR)/mcu-pwr +INC += $(UTEST_HELPERS_DIR)/regmap +INC += $(UTEST_HELPERS_DIR)/system-led +INC += $(UTEST_HELPERS_DIR)/wbmcu_system +INC += $(UTEST_HELPERS_DIR)/wdt-stm32 +INC += $(UTEST_HELPERS_DIR)/wbmz-common +INC += $(UTEST_HELPERS_DIR)/rtc +INC += $(UTEST_HELPERS_DIR)/irq +INC += $(UTEST_HELPERS_DIR)/pwrkey +INC += $(UTEST_HELPERS_DIR)/gpio +INC += $(PROJ_DIR)/include +INC += $(PROJ_DIR)/system/include +INC += $(PROJ_DIR)/libfixmath/libfixmath + +# List of tests +TEST_LIST = wbec_integration_test + +# Compiler defs +DEFS += UNITY_OUTPUT_COLOR +DEFS += "FW_VERSION_STRING='t','e','s','t'" +DEFS += "MODBUS_DEVICE_GIT_INFO=\"test\"" + +# List of targets +TARGETS_LIST = MODEL_WB74 MODEL_WB85 + +include $(PROJ_DIR)/system/build_unittests.mk diff --git a/unittests/wbec-integration/wbec_integration_test.c b/unittests/wbec-integration/wbec_integration_test.c new file mode 100644 index 0000000..898ef93 --- /dev/null +++ b/unittests/wbec-integration/wbec_integration_test.c @@ -0,0 +1,572 @@ +#include "unity.h" + +#include +#include + +#include "config.h" +#include "wbec.h" +#include "wdt.h" +#include "linux-power-control.h" +#include "voltage-monitor.h" +#include "regmap-int.h" +#include "mcu-pwr.h" +#include "utest_gpio.h" +#include "utest_mcu_pwr.h" +#include "utest_pwrkey.h" +#include "utest_regmap.h" +#include "utest_rtc.h" +#include "utest_system_led.h" +#include "utest_systick.h" +#include "utest_voltage_monitor.h" +#include "utest_wbmz_common.h" +#include "utest_irq.h" + +/** + * Интеграционный тест: реальные wbec.c + linux-power-control.c + wdt.c + * работают вместе, как в main loop прошивки. + * + * Поверх модулей построена модель платы: + * - линия +5В процессорного модуля управляется EC_GPIO_LINUX_POWER; + * - PMIC: 3.3В появляется через PMIC_START_MS после подачи 5В + * и пропадает через V33_DECAY_MS после снятия; + * - PMIC может "зависнуть" (авария): 3.3В пропадает и не возвращается, + * пока PMIC не перезапустят снятием 5В или удержанием PWRON; + * - линия PMIC RESET (PWROK): на WB85 (WBEC_HAS_WARM_RESET) удерживает + * в сбросе только SoC (PMIC игнорирует), на WB74 сбрасывает PMIC + * и 3.3В пропадает; + * - SoC "работает", когда есть 5В, 3.3В и линия сброса отпущена; + * может кормить watchdog через regmap, может "зависать". + * + * Главные проверяемые инварианты (свойства отсутствия клина): + * 1. Линия PMIC RESET (PWROK) никогда не удерживается дольше штатной + * длительности (иначе SoC заклинен в сбросе навсегда - именно такой + * отказ наблюдался на стенде: 5В есть, SPL/U-Boot не стартует, + * watchdog больше не перезагружает плату). + * 2. Если Linux не кормит watchdog и переход в standby не запрошен, + * состояние "SoC может стартовать" (5В + 3.3В + сброс отпущен) + * обязано наступать не реже, чем раз в WBEC_WATCHDOG_INITIAL_TIMEOUT_S + * с запасом. Т.е. таймаут watchdog всегда приводит к восстановлению. + */ + +static const gpio_pin_t gpio_5v = { EC_GPIO_LINUX_POWER }; +static const gpio_pin_t gpio_pwron = { EC_GPIO_LINUX_PMIC_PWRON }; +static const gpio_pin_t gpio_nrst = { EC_GPIO_LINUX_PMIC_RESET_PWROK }; + +// Времена модели платы, мс +#define PMIC_START_MS 100 // 5В подано -> 3.3В появилось +#define V33_DECAY_MS 30 // 5В снято -> 3.3В пропало +#define PMIC_REVIVE_5V_OFF_MS 300 // столько без 5В перезапускает зависший PMIC +#define PMIC_REVIVE_PWRON_MS 100 // столько с PWRON перезапускает зависший PMIC +#define SOC_FEED_UPTIME_MS 3000 // аптайм SoC, на котором Linux кормит watchdog + +// Допустимые пределы инвариантов +#if defined(WBEC_HAS_WARM_RESET) + // Тёплый сброс: короткий импульс + запас + #define NRST_ASSERTED_LIMIT_MS (WBEC_WARM_RESET_PULSE_MS + 100) +#else + // Исторический сброс PMIC: удержание до 2 с + запас + #define NRST_ASSERTED_LIMIT_MS (2000 + 100) +#endif +#define NOT_STARTABLE_LIMIT_MS ((WBEC_WATCHDOG_INITIAL_TIMEOUT_S * 1000) + 15000) + +struct sim { + uint32_t now; // сим-время, мс + + // Модель PMIC + bool v33; + bool pmic_crashed; + uint32_t v33_rise_cnt; + uint32_t v33_fall_cnt; + uint32_t revive_5v_off_cnt; + uint32_t revive_pwron_cnt; + + // Модель SoC + bool soc_feeds; // Linux жив и кормит watchdog + uint32_t soc_hang_at_uptime_ms; // аптайм, на котором Linux зависает (0 = не зависает) + uint32_t soc_run_since; // 0 = SoC в сбросе/без питания + uint32_t soc_boot_count; // сколько раз SoC начинал загрузку + + // Отложенные события + uint32_t pmic_crash_at; // авария PMIC в этот момент сим-времени (0 = нет) + uint32_t powerctrl_at; // запись в POWER_CTRL в этот момент (0 = нет) + bool powerctrl_off; + bool powerctrl_reboot; + bool powerctrl_reset_pmic; + + // Наблюдения + bool prev_p5v; + bool prev_nrst; + uint32_t rail_off_edges; // 5В: переходы 1 -> 0 + uint32_t warm_pulses; // завершённые импульсы на линии сброса при включённом 5В + + // Инварианты + bool check_invariants; + bool allow_standby; + uint32_t nrst_asserted_ms; + uint32_t max_nrst_asserted_ms; + uint32_t not_startable_ms; + uint32_t max_not_startable_ms; + bool standby_requested; +}; + +static struct sim sim; + +// Момент последнего входа wbec в состояние WORKING (перехват weak-функции) +static uint32_t working_entry_count; +static uint32_t last_working_entry_time; + +void linux_poweron_handler(void) +{ + working_entry_count++; + last_working_entry_time = sim.now; +} + +// ==================== Локальные заглушки wbec ==================== + +static bool alarm_enabled; + +bool rtc_alarm_is_alarm_enabled(void) { return alarm_enabled; } + +bool temperature_control_is_temperature_ready(void) { return true; } +int16_t temperature_control_get_temperature_c_x100(void) { return 2500; } + +void usart_tx_buf_blocking(const void * buf, size_t size) { (void)buf; (void)size; } +void buzzer_beep(uint16_t freq, uint16_t duration_ms) { (void)freq; (void)duration_ms; } + +// ==================== Модель платы ==================== + +// Кормление watchdog из "Linux": установка бита reset в регионе WDT +static void soc_feed_watchdog(void) +{ + struct REGMAP_WDT w; + if (!utest_regmap_get_region_data(REGMAP_REGION_WDT, &w, sizeof(w))) { + memset(&w, 0, sizeof(w)); + w.timeout = WBEC_WATCHDOG_INITIAL_TIMEOUT_S; + } + w.reset = 1; + regmap_set_region_data(REGMAP_REGION_WDT, &w, sizeof(w)); + utest_regmap_mark_region_changed(REGMAP_REGION_WDT); +} + +static void sim_fail(const char * why) +{ + char msg[256]; + snprintf(msg, sizeof(msg), + "%s: t=%u ms, 5V=%u nRST=%u V33=%u pmic_crashed=%u soc_boots=%u " + "rail_off=%u warm_pulses=%u nrst_ms=%u not_startable_ms=%u", + why, sim.now, + (unsigned)(utest_gpio_get_output_state(gpio_5v) != 0), + (unsigned)(utest_gpio_get_output_state(gpio_nrst) != 0), + (unsigned)sim.v33, (unsigned)sim.pmic_crashed, sim.soc_boot_count, + sim.rail_off_edges, sim.warm_pulses, + sim.nrst_asserted_ms, sim.not_startable_ms); + TEST_FAIL_MESSAGE(msg); +} + +// Один тик = 1 мс. Порядок вызовов повторяет main loop прошивки: +// wdt_do_periodic_work -> vmon -> linux_cpu_pwr_seq_do_periodic_work -> wbec_do_periodic_work +static void sim_tick(void) +{ + sim.now++; + utest_systick_advance_time_ms(1); + + // Отложенные события начала тика + if ((sim.pmic_crash_at != 0) && (sim.now >= sim.pmic_crash_at)) { + sim.pmic_crash_at = 0; + sim.pmic_crashed = true; + sim.v33 = false; // авария: 3.3В пропадает мгновенно + } + if ((sim.powerctrl_at != 0) && (sim.now >= sim.powerctrl_at)) { + sim.powerctrl_at = 0; + struct REGMAP_POWER_CTRL p = { + .off = sim.powerctrl_off ? 1 : 0, + .reboot = sim.powerctrl_reboot ? 1 : 0, + .reset_pmic = sim.powerctrl_reset_pmic ? 1 : 0, + }; + regmap_set_region_data(REGMAP_REGION_POWER_CTRL, &p, sizeof(p)); + utest_regmap_mark_region_changed(REGMAP_REGION_POWER_CTRL); + } + + bool p5v = utest_gpio_get_output_state(gpio_5v) != 0; + bool pwron = utest_gpio_get_output_state(gpio_pwron) != 0; + bool nrst = utest_gpio_get_output_state(gpio_nrst) != 0; + + // Перезапуск зависшего PMIC + if (sim.pmic_crashed) { + sim.revive_5v_off_cnt = p5v ? 0 : (sim.revive_5v_off_cnt + 1); + sim.revive_pwron_cnt = pwron ? (sim.revive_pwron_cnt + 1) : 0; + if ((sim.revive_5v_off_cnt >= PMIC_REVIVE_5V_OFF_MS) || + (sim.revive_pwron_cnt >= PMIC_REVIVE_PWRON_MS)) + { + sim.pmic_crashed = false; + sim.revive_5v_off_cnt = 0; + sim.revive_pwron_cnt = 0; + } + } + + // Динамика 3.3В + bool v33_target = p5v && !sim.pmic_crashed; +#if !defined(WBEC_HAS_WARM_RESET) + // На WB74 линия PMIC RESET сбрасывает PMIC: выходы PMIC отключаются + v33_target = v33_target && !nrst; +#endif + if (v33_target) { + sim.v33_fall_cnt = 0; + if (!sim.v33 && (++sim.v33_rise_cnt >= PMIC_START_MS)) { + sim.v33 = true; + } + } else { + sim.v33_rise_cnt = 0; + if (sim.v33 && (++sim.v33_fall_cnt >= V33_DECAY_MS)) { + sim.v33 = false; + } + } + + // Модель SoC + bool soc_startable = p5v && sim.v33 && !nrst; + if (!soc_startable) { + sim.soc_run_since = 0; + } else if (sim.soc_run_since == 0) { + sim.soc_run_since = sim.now; + sim.soc_boot_count++; + } else if (sim.soc_feeds) { + uint32_t uptime = sim.now - sim.soc_run_since; + bool hung = (sim.soc_hang_at_uptime_ms != 0) && (uptime >= sim.soc_hang_at_uptime_ms); + if (!hung && (uptime >= SOC_FEED_UPTIME_MS) && + ((uptime - SOC_FEED_UPTIME_MS) % 30000 == 0)) + { + soc_feed_watchdog(); + } + } + + // Обновление vmon (модель измерений) + utest_vmon_set_ready(true); + utest_vmon_set_ch_status(VMON_CHANNEL_V50, true); + utest_vmon_set_ch_status(VMON_CHANNEL_V33, sim.v33); + utest_vmon_set_ch_status(VMON_CHANNEL_V_IN, true); + + // Периодические задачи в порядке main loop + wdt_do_periodic_work(); + linux_cpu_pwr_seq_do_periodic_work(); + wbec_do_periodic_work(); + + // Наблюдения за фронтами (после работы модулей) + bool p5v_after = utest_gpio_get_output_state(gpio_5v) != 0; + bool nrst_after = utest_gpio_get_output_state(gpio_nrst) != 0; + if (sim.prev_p5v && !p5v_after) { + sim.rail_off_edges++; + } + if (sim.prev_nrst && !nrst_after && p5v_after) { + sim.warm_pulses++; + } + sim.prev_p5v = p5v_after; + sim.prev_nrst = nrst_after; + + if (utest_mcu_get_standby_wakeup_time() != 0) { + sim.standby_requested = true; + } + + // Инварианты + if (sim.check_invariants) { + sim.nrst_asserted_ms = nrst_after ? (sim.nrst_asserted_ms + 1) : 0; + if (sim.nrst_asserted_ms > sim.max_nrst_asserted_ms) { + sim.max_nrst_asserted_ms = sim.nrst_asserted_ms; + } + if (sim.nrst_asserted_ms > NRST_ASSERTED_LIMIT_MS) { + sim_fail("WEDGE: PMIC RESET (PWROK) line is latched, SoC is held in reset"); + } + + if (!sim.standby_requested) { + bool startable_after = p5v_after && sim.v33 && !nrst_after; + sim.not_startable_ms = startable_after ? 0 : (sim.not_startable_ms + 1); + if (sim.not_startable_ms > sim.max_not_startable_ms) { + sim.max_not_startable_ms = sim.not_startable_ms; + } + if (sim.not_startable_ms > NOT_STARTABLE_LIMIT_MS) { + sim_fail("WEDGE: watchdog did not recover the SoC in time"); + } + } + + if (!sim.allow_standby && sim.standby_requested) { + sim_fail("Unexpected standby request"); + } + } +} + +static void sim_run_ms(uint32_t duration_ms) +{ + for (uint32_t i = 0; i < duration_ms; i++) { + sim_tick(); + } +} + +// ==================== Подготовка сценариев ==================== + +void utest_wbec_reset_state(void); +void utest_linux_power_control_reset_state(void); +void utest_wdt_module_reset_state(void); + +void setUp(void) +{ + memset(&sim, 0, sizeof(sim)); + sim.check_invariants = true; + working_entry_count = 0; + last_working_entry_time = 0; + alarm_enabled = false; + + utest_wbec_reset_state(); + utest_linux_power_control_reset_state(); + utest_wdt_module_reset_state(); + utest_gpio_reset_instances(); + utest_mcu_reset(); + utest_systick_set_time_ms(1000); + utest_vmon_reset(); + utest_regmap_reset(); + utest_system_led_reset(); + utest_wbmz_common_reset(); + utest_pwrkey_reset(); + utest_rtc_reset(); + utest_irq_reset(); +} + +void tearDown(void) +{ +} + +// Штатное включение платы до состояния WORKING +static void sim_boot_to_working(void) +{ + utest_mcu_set_poweron_reason(MCU_POWERON_REASON_POWER_ON); + utest_vmon_set_ready(true); + utest_vmon_set_ch_status(VMON_CHANNEL_V50, true); + + wbec_init(); + + // WAIT_STARTUP -> VOLTAGE_CHECK -> POWER_ON_SEQUENCE_WAIT -> WORKING + uint32_t guard = 10000; + while ((working_entry_count == 0) && (guard--)) { + sim_tick(); + } + TEST_ASSERT_NOT_EQUAL_MESSAGE(0, working_entry_count, + "Board must reach WORKING state during normal boot"); +} + +// Момент первого срабатывания watchdog после входа в WORKING: +// wdt перезапущен в том же тике, что и вход в WORKING, таймаут наступает +// строго через WBEC_WATCHDOG_INITIAL_TIMEOUT_S * 1000 + 1 мс +static uint32_t expected_wdt_timeout_time(void) +{ + return last_working_entry_time + (WBEC_WATCHDOG_INITIAL_TIMEOUT_S * 1000) + 1; +} + +// ==================== Тесты ==================== + +// Сценарий: здоровая система. Linux загружается и кормит watchdog. +// Ожидание: ни одного сброса за 10 минут, инварианты не нарушены. +static void test_healthy_linux_runs_without_resets(void) +{ + sim.soc_feeds = true; + sim.soc_hang_at_uptime_ms = 0; + + sim_boot_to_working(); + sim_run_ms(600000); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, sim.soc_boot_count, + "SoC must boot exactly once"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, sim.rail_off_edges, + "5V rail must not be cycled when Linux feeds the watchdog"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, sim.warm_pulses, + "Reset line must not be pulsed when Linux feeds the watchdog"); +} + +// Сценарий со стенда: Linux зависает через 3.2 с после старта на КАЖДОЙ загрузке, +// успев один раз покормить watchdog (драйвер EC загрузился - система "жива"). +// Ожидание: каждый таймаут восстанавливает SoC, клина нет. +static void test_linux_hangs_every_boot_watchdog_always_recovers(void) +{ + sim.soc_feeds = true; + sim.soc_hang_at_uptime_ms = 3200; + + sim_boot_to_working(); + // ~7 циклов "загрузка - зависание - таймаут" по ~123 c + sim_run_ms(900000); + + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(5, sim.soc_boot_count, + "Watchdog must keep restarting the hanging SoC"); +#if defined(WBEC_HAS_WARM_RESET) + // Кормление сбрасывает эскалацию: каждый сброс - тёплый, DRAM сохраняется + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(5, sim.warm_pulses, + "Every recovery must be a warm reset while feeds arrive between hangs"); +#endif +} + +// Сценарий: SoC вообще не загружается (не кормит watchdog никогда). +// Ожидание: восстановление продолжается неограниченно долго, клина нет. +// На WB85 эскалация чередуется: тёплый -> жёсткий -> тёплый -> ... +// (после жёсткого сброса начинается новая загрузка, и первое зависание +// в ней снова заслуживает тёплого сброса с сохранением DRAM/ramoops) +static void test_dead_linux_escalation_never_wedges(void) +{ + sim.soc_feeds = false; + + sim_boot_to_working(); + // 4 таймаута: ~500 c + sim_run_ms(500000); + +#if defined(WBEC_HAS_WARM_RESET) + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(2, sim.warm_pulses, + "Escalation must keep trying warm resets (warm/hard alternation)"); + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(1, sim.rail_off_edges, + "Escalation must reach the hard power cycle stage"); +#else + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(3, sim.rail_off_edges, + "Every watchdog timeout must hard-cycle the power"); +#endif +} + +// Сценарий-КЛИН (авария со стенда): таймаут watchdog приходит в том же тике, +// в котором пропало 3.3В (авария PMIC). +// До исправления (WB85): обработчик таймаута запускает тёплый сброс +// (линия PMIC RESET (PWROK) взводится), затем в том же проходе ветка контроля +// 3.3В вызывает hard_reset, который затирает состояние PS_WARM_RESET_PULSE. +// Импульс никогда не завершается, линия сброса остаётся взведённой НАВСЕГДА: +// SoC заклинен в сбросе (нет SPL/U-Boot), 5В включено, watchdog "работает", +// но каждый его жёсткий сброс не отпускает линию. Плата мертва до снятия питания. +static void test_wdt_timeout_racing_pmic_crash_does_not_latch_reset_line(void) +{ + sim.soc_feeds = false; + + sim_boot_to_working(); + sim.pmic_crash_at = expected_wdt_timeout_time(); + + // 400 c после аварии: восстановление + очередные циклы эскалации + sim_run_ms((expected_wdt_timeout_time() - sim.now) + 400000); + + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(2, sim.soc_boot_count, + "SoC must become startable again after the racing PMIC crash"); +} + +// Сценарий-КЛИН: запрос reset_pmic из Linux приходит в том же тике, +// что и таймаут watchdog. +// До исправления: запрос запускает тёплый сброс (WB85) / сброс PMIC (WB74) +// с взведением линии PMIC RESET (PWROK), а обработчик таймаута в том же +// проходе вызывает hard_reset - линия остаётся взведённой навсегда. +static void test_pmic_reset_request_racing_wdt_timeout_does_not_latch(void) +{ + sim.soc_feeds = false; + + sim_boot_to_working(); + +#if defined(WBEC_HAS_WARM_RESET) + // Для жёсткой ветки эскалации нужен второй таймаут без кормления: + // первый таймаут (тёплый сброс) уже был + uint32_t first_timeout = expected_wdt_timeout_time(); + sim_run_ms((first_timeout - sim.now) + 1000); + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(2, working_entry_count, + "Board must re-enter WORKING after the first warm reset"); +#endif + + sim.powerctrl_at = expected_wdt_timeout_time(); + sim.powerctrl_reset_pmic = true; + + sim_run_ms((expected_wdt_timeout_time() - sim.now) + 400000); + + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(2, sim.soc_boot_count, + "SoC must become startable again after reset_pmic request racing the timeout"); +} + +// Сценарий: развёртка момента аварии PMIC вокруг таймаута watchdog. +// Авария попадает во все фазы: до таймаута (штатная ветка потери 3.3В), +// в тот же тик (гонка), во время импульса тёплого сброса, в шаги включения +// PS_ON_STEP*, в паузу жёсткого сброса. +// Ожидание: ни один момент аварии не приводит к клину. +static void test_pmic_crash_timing_sweep_never_wedges(void) +{ + static const int32_t offsets_ms[] = { + -1500, -1000, -400, -100, -10, -1, + 0, + 1, 10, 50, 99, 100, 101, 150, 400, 600, 1000, 1101, 1500, 2500, + }; + + for (unsigned i = 0; i < sizeof(offsets_ms) / sizeof(offsets_ms[0]); i++) { + setUp(); + sim.soc_feeds = false; + + sim_boot_to_working(); + sim.pmic_crash_at = expected_wdt_timeout_time() + offsets_ms[i]; + + // 300 c после аварии достаточно для восстановления с запасом + sim_run_ms((sim.pmic_crash_at - sim.now) + 300000); + + char msg[128]; + snprintf(msg, sizeof(msg), + "SoC must be restartable after PMIC crash at timeout%+d ms", + (int)offsets_ms[i]); + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(2, sim.soc_boot_count, msg); + } +} + +// Сценарий: запрос poweroff из Linux (будильник взведён) приходит в том же +// тике, что и таймаут watchdog. +// Ожидание: выключение выигрывает - плата выключается и уходит в standby, +// таймаут не "воскрешает" её. +// До исправления: обработчик таймаута в том же проходе запускал сброс +// поверх начатого выключения, и плата включалась обратно. +static void test_poweroff_request_racing_wdt_timeout_stays_off(void) +{ + sim.soc_feeds = false; + sim.allow_standby = true; + alarm_enabled = true; + + sim_boot_to_working(); + + sim.powerctrl_at = expected_wdt_timeout_time(); + sim.powerctrl_off = true; + + uint32_t until_req = expected_wdt_timeout_time() - sim.now; + sim_run_ms(until_req + 2000); + + TEST_ASSERT_TRUE_MESSAGE(sim.standby_requested, + "Poweroff request must lead to standby even when racing a watchdog timeout"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, utest_gpio_get_output_state(gpio_5v), + "5V rail must stay off after poweroff, watchdog must not resurrect the board"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, sim.soc_boot_count, + "SoC must not be restarted after poweroff request"); +} + +#if defined(WBEC_HAS_WARM_RESET) +// Сценарий: SoC никогда не кормит watchdog - проверка чередования эскалации. +// Ожидание: тёплый -> жёсткий -> тёплый -> жёсткий (каждый жёсткий сброс +// начинает новый цикл загрузки, первое зависание в нём получает тёплый сброс). +static void test_escalation_alternates_warm_and_hard(void) +{ + sim.soc_feeds = false; + + sim_boot_to_working(); + // 4 таймаута: 4 * ~123 c + sim_run_ms(500000); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(2, sim.warm_pulses, + "Timeouts 1 and 3 must be warm resets"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(2, sim.rail_off_edges, + "Timeouts 2 and 4 must be hard power cycles"); +} +#endif + +int main(void) +{ + UNITY_BEGIN(); + + RUN_TEST(test_healthy_linux_runs_without_resets); + RUN_TEST(test_linux_hangs_every_boot_watchdog_always_recovers); + RUN_TEST(test_dead_linux_escalation_never_wedges); + RUN_TEST(test_wdt_timeout_racing_pmic_crash_does_not_latch_reset_line); + RUN_TEST(test_pmic_reset_request_racing_wdt_timeout_does_not_latch); + RUN_TEST(test_pmic_crash_timing_sweep_never_wedges); + RUN_TEST(test_poweroff_request_racing_wdt_timeout_stays_off); +#if defined(WBEC_HAS_WARM_RESET) + RUN_TEST(test_escalation_alternates_warm_and_hard); +#endif + + return UNITY_END(); +} diff --git a/unittests/wbec/wbec_test.c b/unittests/wbec/wbec_test.c index d80633c..1ff8637 100644 --- a/unittests/wbec/wbec_test.c +++ b/unittests/wbec/wbec_test.c @@ -1100,6 +1100,137 @@ static void test_periodic_working_wdt_feed_resets_escalation(void) } #endif // WBEC_HAS_WARM_RESET +// Сценарий: таймаут WDT и потеря 3.3В (и 5В) приходят в одном проходе. +// Ожидание: выполняется ровно ОДНО действие с питанием - сброс по watchdog. +// Ветка контроля 3.3В не должна запускать вторую последовательность поверх +// начатой: до исправления она вызывала hard_off/hard_reset, который прерывал +// начатый тёплый сброс и оставлял линию PMIC RESET (PWROK) взведённой навсегда. +static void test_periodic_working_wdt_timeout_and_v33_loss_single_action(void) +{ + drive_to_working_state(); + + utest_wdt_set_timed_out(true); + utest_vmon_set_ch_status(VMON_CHANNEL_V33, false); + utest_vmon_set_ch_status(VMON_CHANNEL_V50, false); + wbec_do_periodic_work(); + +#if defined(WBEC_HAS_WARM_RESET) + TEST_ASSERT_TRUE_MESSAGE(utest_linux_pwr_get_warm_reset_called(), + "warm_reset must be called on WDT timeout"); + TEST_ASSERT_FALSE_MESSAGE(utest_linux_pwr_get_hard_reset_called(), + "hard_reset must NOT be called in the same pass as warm_reset"); +#else + TEST_ASSERT_TRUE_MESSAGE(utest_linux_pwr_get_hard_reset_called(), + "hard_reset must be called on WDT timeout"); +#endif + TEST_ASSERT_FALSE_MESSAGE(utest_linux_pwr_get_hard_off_called(), + "V33/V50 loss check must not run in the same pass as a WDT reset"); +} + +// Сценарий: запрос reset_pmic из Linux и таймаут WDT приходят в одном проходе. +// Ожидание: обрабатывается только запрос из Linux, таймаут откладывается +// (флаг не теряется - он поглощается перезапуском watchdog при выходе +// из POWER_ON_SEQUENCE_WAIT). До исправления обработчик таймаута вызывал +// hard_reset поверх начатого тёплого сброса / сброса PMIC и оставлял +// линию PMIC RESET (PWROK) взведённой навсегда. +static void test_periodic_working_pmic_reset_request_and_wdt_timeout_single_action(void) +{ +#if defined(WBEC_HAS_WARM_RESET) + drive_to_working_state(); + + // Первый таймаут без кормления → эскалация взведена (следующий сброс - жёсткий) + utest_wdt_set_timed_out(true); + wbec_do_periodic_work(); + TEST_ASSERT_TRUE_MESSAGE(utest_linux_pwr_get_warm_reset_called(), + "warm_reset must be called on first WDT timeout"); + utest_linux_pwr_clear_warm_reset_called(); + drive_back_to_working_state(); + + // Запрос reset_pmic и таймаут в одном проходе + set_power_ctrl_request(false, false, true); + utest_wdt_set_timed_out(true); + wbec_do_periodic_work(); + + TEST_ASSERT_TRUE_MESSAGE(utest_linux_pwr_get_warm_reset_called(), + "warm_reset must be called on reset_pmic request"); + TEST_ASSERT_FALSE_MESSAGE(utest_linux_pwr_get_hard_reset_called(), + "hard_reset must NOT be called in the same pass as reset_pmic request"); +#else + drive_to_working_state(); + + set_power_ctrl_request(false, false, true); + utest_wdt_set_timed_out(true); + wbec_do_periodic_work(); + + TEST_ASSERT_TRUE_MESSAGE(utest_linux_pwr_get_reset_pmic_called(), + "reset_pmic must be called on reset_pmic request"); + TEST_ASSERT_FALSE_MESSAGE(utest_linux_pwr_get_hard_reset_called(), + "hard_reset must NOT be called in the same pass as reset_pmic request"); +#endif +} + +// Сценарий: запрос poweroff из Linux (будильник взведён) и таймаут WDT +// приходят в одном проходе. +// Ожидание: выключение выигрывает, watchdog не "воскрешает" плату. +static void test_periodic_working_poweroff_request_and_wdt_timeout_powers_off(void) +{ + drive_to_working_state(); + + utest_rtc_alarm_set_enabled(true); + set_power_ctrl_request(true, false, false); + utest_wdt_set_timed_out(true); + wbec_do_periodic_work(); + + TEST_ASSERT_TRUE_MESSAGE(utest_linux_pwr_get_hard_off_called(), + "hard_off must be called on poweroff request"); + TEST_ASSERT_FALSE_MESSAGE(utest_linux_pwr_get_hard_reset_called(), + "hard_reset must NOT be called after poweroff started"); +#if defined(WBEC_HAS_WARM_RESET) + TEST_ASSERT_FALSE_MESSAGE(utest_linux_pwr_get_warm_reset_called(), + "warm_reset must NOT be called after poweroff started"); +#endif +} + +#if defined(WBEC_HAS_WARM_RESET) +// Сценарий: три таймаута WDT подряд без кормления. +// Ожидание: тёплый → жёсткий → снова тёплый. Жёсткий сброс - последняя +// ступень эскалации, после него начинается новый цикл загрузки, и первое +// зависание в нём снова заслуживает тёплого сброса (сохранение DRAM/ramoops). +// До исправления эскалация "залипала": после первого жёсткого сброса все +// последующие таймауты давали только жёсткий сброс. +static void test_periodic_working_wdt_third_timeout_warm_again(void) +{ + drive_to_working_state(); + + // Первый таймаут → warm + utest_wdt_set_timed_out(true); + wbec_do_periodic_work(); + TEST_ASSERT_TRUE_MESSAGE(utest_linux_pwr_get_warm_reset_called(), + "warm_reset must be called on first WDT timeout"); + utest_linux_pwr_clear_warm_reset_called(); + + // Второй таймаут без кормления → hard + drive_back_to_working_state(); + utest_wdt_set_timed_out(true); + wbec_do_periodic_work(); + TEST_ASSERT_TRUE_MESSAGE(utest_linux_pwr_get_hard_reset_called(), + "hard_reset must be called on second WDT timeout"); + + // Третий таймаут → снова warm (эскалация начинается заново) + utest_linux_pwr_reset(); + drive_back_to_working_state(); + utest_wdt_set_timed_out(true); + wbec_do_periodic_work(); + + TEST_ASSERT_TRUE_MESSAGE(utest_linux_pwr_get_warm_reset_called(), + "warm_reset must be called again on the timeout after a hard reset"); + TEST_ASSERT_FALSE_MESSAGE(utest_linux_pwr_get_hard_reset_called(), + "hard_reset must NOT be called on the timeout after a hard reset"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_WATCHDOG_WARM, get_poweron_reason_from_regmap(), + "poweron_reason must be REASON_WATCHDOG_WARM again"); +} +#endif // WBEC_HAS_WARM_RESET + // Сценарий: пропало 3.3В и 5В одновременно → hard_off (питание выдернуто). static void test_periodic_working_v33_and_v50_lost(void) { @@ -1322,7 +1453,12 @@ int main(void) #if defined(WBEC_HAS_WARM_RESET) RUN_TEST(test_periodic_working_wdt_timeout_second_time_hard); RUN_TEST(test_periodic_working_wdt_feed_resets_escalation); + RUN_TEST(test_periodic_working_wdt_third_timeout_warm_again); #endif + // Гонки в одном проходе: только одно действие с питанием за проход + RUN_TEST(test_periodic_working_wdt_timeout_and_v33_loss_single_action); + RUN_TEST(test_periodic_working_pmic_reset_request_and_wdt_timeout_single_action); + RUN_TEST(test_periodic_working_poweroff_request_and_wdt_timeout_powers_off); RUN_TEST(test_periodic_working_v33_and_v50_lost); RUN_TEST(test_periodic_working_v33_lost_wbmz_vbat_low); RUN_TEST(test_periodic_working_v33_lost_wbmz_vbat_ok_uses_generic_path); From 6361c93c89d0bd8379a9b4466ce0f2b86b44c647 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Sun, 5 Jul 2026 21:32:03 +0300 Subject: [PATCH 03/12] Add suspend mode: announced sleep window via SUSPEND_CTRL regmap Linux writes the expected sleep duration in seconds to a new regmap region SUSPEND_CTRL (0xA4) before suspending. While the mode is active: - the 3.3V-loss monitor is skipped (the firmware suspend path powers DCDC1 down deliberately, which is otherwise indistinguishable from the 'PMIC unexpectedly off' failure); - watchdog timeouts are ignored, replaced by a deadline of the requested duration plus 10 s: a system that never wakes gets the usual warm-reset (WB85) / power-cycle (WB74) recovery. The mode ends on the first watchdog feed after resume (no extra kernel work needed), an explicit write of 0, or the deadline. Co-Authored-By: Claude Fable 5 --- include/regmap-structs.h | 4 +++ src/wbec.c | 75 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/include/regmap-structs.h b/include/regmap-structs.h index b95fa49..2a69272 100644 --- a/include/regmap-structs.h +++ b/include/regmap-structs.h @@ -97,6 +97,10 @@ /* -//- */ uint16_t reset_pmic : 1; \ ) \ /* Addr Name RO/RW */ \ + m( 0xA4, SUSPEND_CTRL, RW, \ + /* 0xA4 */ uint16_t timeout_s; \ + ) \ + /* Addr Name RO/RW */ \ m( 0xB0, IRQ_FLAGS, RO, \ /* 0xB0 */ uint16_t irqs; \ ) \ diff --git a/src/wbec.c b/src/wbec.c index 3b89dc0..937cd68 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -84,6 +84,12 @@ struct wbec_ctx { // Если после тёплого сброса Linux так и не начал сбрасывать watchdog, // следующий таймаут приведёт к жёсткому сбросу по питанию bool wd_warm_reset_attempted; + // Режим suspend: Linux объявил окно сна через SUSPEND_CTRL. + // В этом режиме потеря 3.3В ожидаема (BL31 гасит DCDC1), а + // watchdog-эскалация заменена на дедлайн запрошенной длительности. + bool suspend_mode; + systime_t suspend_entry_timestamp; + uint32_t suspend_timeout_ms; }; static struct wbec_ctx wbec_ctx; @@ -493,6 +499,24 @@ void wbec_do_periodic_work(void) } } + // Запрос режима suspend из Linux: запись таймаута (секунды) в + // SUSPEND_CTRL включает режим, запись 0 - выключает. + { + struct REGMAP_SUSPEND_CTRL s; + if (regmap_get_data_if_region_changed(REGMAP_REGION_SUSPEND_CTRL, &s, sizeof(s))) { + if (s.timeout_s > 0) { + wbec_ctx.suspend_mode = true; + wbec_ctx.suspend_entry_timestamp = systick_get_system_time_ms(); + // Запас 10 с поверх запрошенной длительности + wbec_ctx.suspend_timeout_ms = (uint32_t)s.timeout_s * 1000 + 10000; + console_print_w_prefix("Suspend mode: on\r\n"); + } else if (wbec_ctx.suspend_mode) { + wbec_ctx.suspend_mode = false; + console_print_w_prefix("Suspend mode: off (request)\r\n"); + } + } + } + if (linux_powerctrl_req == LINUX_POWERCTRL_OFF) { // Если прилетел запрос из линукса на выключение // Это была выполнена команда `poweroff` или `rtcwake -m off` @@ -586,11 +610,35 @@ void wbec_do_periodic_work(void) #if defined(WBEC_HAS_WARM_RESET) // Если Linux сбрасывает watchdog - система жива, - // разрешаем следующую попытку тёплого сброса + // разрешаем следующую попытку тёплого сброса. + // Это же событие завершает режим suspend: после пробуждения + // Linux первым делом снова начинает кормить watchdog. if (wdt_handle_fed()) { wbec_ctx.wd_warm_reset_attempted = false; + if (wbec_ctx.suspend_mode) { + wbec_ctx.suspend_mode = false; + console_print_w_prefix("Suspend mode: off (watchdog fed)\r\n"); + } } + if (wbec_ctx.suspend_mode) { + // Во время объявленного сна watchdog-таймауты игнорируются + // (Linux заморожен и не кормит намеренно), но запрошенная + // длительность сна + запас служит дедлайном: если система + // не проснулась - обычное восстановление тёплым сбросом. + (void)wdt_handle_timed_out(); + if (systick_get_time_since_timestamp(wbec_ctx.suspend_entry_timestamp) > + wbec_ctx.suspend_timeout_ms) { + wbec_ctx.suspend_mode = false; + wbec_ctx.wd_warm_reset_attempted = true; + wbec_ctx.poweron_reason = REASON_WATCHDOG_WARM; + console_print("\r\n\n"); + console_print_w_prefix("Suspend deadline passed, system did not wake up - warm reset.\r\n"); + linux_cpu_pwr_seq_warm_reset(); + new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); + break; + } + } else // Если сработал WDT - сначала пробуем тёплый сброс (DRAM сохраняется, // логи ramoops можно будет прочитать после перезагрузки). // Если после тёплого сброса система не ожила (Linux не сбросил @@ -615,6 +663,27 @@ void wbec_do_periodic_work(void) new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); } #else + // Завершение режима suspend по первому кормлению watchdog + if (wdt_handle_fed() && wbec_ctx.suspend_mode) { + wbec_ctx.suspend_mode = false; + console_print_w_prefix("Suspend mode: off (watchdog fed)\r\n"); + } + + if (wbec_ctx.suspend_mode) { + // Во время объявленного сна watchdog-таймауты игнорируются; + // дедлайн - запрошенная длительность сна + запас. + (void)wdt_handle_timed_out(); + if (systick_get_time_since_timestamp(wbec_ctx.suspend_entry_timestamp) > + wbec_ctx.suspend_timeout_ms) { + wbec_ctx.suspend_mode = false; + wbec_ctx.poweron_reason = REASON_WATCHDOG; + console_print("\r\n\n"); + console_print_w_prefix("Suspend deadline passed, system did not wake up - reset power.\r\n"); + linux_cpu_pwr_seq_hard_reset(); + new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); + break; + } + } else // Если сработал WDT - перезагружаемся по питанию if (wdt_handle_timed_out()) { wbec_ctx.poweron_reason = REASON_WATCHDOG; @@ -637,7 +706,9 @@ void wbec_do_periodic_work(void) // Это происходит, например, при питании через плохой USB кабель. // В результате PMIC выключается, но питание на линии 5В остаётся. // Ограничение по числу попыток нужно, чтобы избежать циклического перезапуска. - if (!vmon_get_ch_status(VMON_CHANNEL_V33)) { + // В режиме suspend потеря 3.3В ожидаема: BL31 отключает DCDC1 + // на время сна - проверку пропускаем. + if (!wbec_ctx.suspend_mode && !vmon_get_ch_status(VMON_CHANNEL_V33)) { console_print_w_prefix("3.3V is lost\r\n"); if (!vmon_get_ch_status(VMON_CHANNEL_V50)) { From 9aaf8d4e0e9489d9e0046826dc9e80f1762ad61f Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Sun, 5 Jul 2026 22:00:36 +0300 Subject: [PATCH 04/12] suspend mode: arm the watchdog-feed exit only after the sleep started MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The watchdog daemon keeps feeding for another second or two between the SUSPEND_CTRL write (systemd-sleep pre hook) and the actual freeze, so exiting the mode on any feed cancelled it before the sleep even began — and the 3.3V monitor then killed the suspended board as before. The feed-exit is now armed only once the EC has actually seen 3.3V drop during the announced window; aborted suspends are covered by the explicit zero-write from the post hook and by the deadline. Co-Authored-By: Claude Fable 5 --- src/wbec.c | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/wbec.c b/src/wbec.c index 937cd68..88e9153 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -90,6 +90,11 @@ struct wbec_ctx { bool suspend_mode; systime_t suspend_entry_timestamp; uint32_t suspend_timeout_ms; + // Взводится, когда во время suspend реально пропало 3.3В. + // До этого момента кормления watchdog НЕ завершают режим: + // между записью в SUSPEND_CTRL и заморозкой системы демон + // watchdog продолжает кормить ещё ~1-2 секунды. + bool suspend_started; }; static struct wbec_ctx wbec_ctx; @@ -506,6 +511,7 @@ void wbec_do_periodic_work(void) if (regmap_get_data_if_region_changed(REGMAP_REGION_SUSPEND_CTRL, &s, sizeof(s))) { if (s.timeout_s > 0) { wbec_ctx.suspend_mode = true; + wbec_ctx.suspend_started = false; wbec_ctx.suspend_entry_timestamp = systick_get_system_time_ms(); // Запас 10 с поверх запрошенной длительности wbec_ctx.suspend_timeout_ms = (uint32_t)s.timeout_s * 1000 + 10000; @@ -615,7 +621,10 @@ void wbec_do_periodic_work(void) // Linux первым делом снова начинает кормить watchdog. if (wdt_handle_fed()) { wbec_ctx.wd_warm_reset_attempted = false; - if (wbec_ctx.suspend_mode) { + // Кормление завершает режим suspend только после того, как + // сон реально начался (3.3В пропадало): до заморозки системы + // демон watchdog продолжает кормить. + if (wbec_ctx.suspend_mode && wbec_ctx.suspend_started) { wbec_ctx.suspend_mode = false; console_print_w_prefix("Suspend mode: off (watchdog fed)\r\n"); } @@ -663,8 +672,9 @@ void wbec_do_periodic_work(void) new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); } #else - // Завершение режима suspend по первому кормлению watchdog - if (wdt_handle_fed() && wbec_ctx.suspend_mode) { + // Завершение режима suspend по первому кормлению watchdog - + // только после реального начала сна (3.3В пропадало) + if (wdt_handle_fed() && wbec_ctx.suspend_mode && wbec_ctx.suspend_started) { wbec_ctx.suspend_mode = false; console_print_w_prefix("Suspend mode: off (watchdog fed)\r\n"); } @@ -707,7 +717,11 @@ void wbec_do_periodic_work(void) // В результате PMIC выключается, но питание на линии 5В остаётся. // Ограничение по числу попыток нужно, чтобы избежать циклического перезапуска. // В режиме suspend потеря 3.3В ожидаема: BL31 отключает DCDC1 - // на время сна - проверку пропускаем. + // на время сна - проверку пропускаем, но запоминаем факт + // пропадания: с этого момента сон действительно начался. + if (wbec_ctx.suspend_mode && !vmon_get_ch_status(VMON_CHANNEL_V33)) { + wbec_ctx.suspend_started = true; + } if (!wbec_ctx.suspend_mode && !vmon_get_ch_status(VMON_CHANNEL_V33)) { console_print_w_prefix("3.3V is lost\r\n"); From b5e74799c62fcd0f4959130ad5aa93b0a9ee2dcd Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Sun, 5 Jul 2026 23:40:17 +0300 Subject: [PATCH 05/12] Add suspend-to-off mode: EC wakes the sleeping PMIC by alarm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SUSPEND_CTRL gains an off_mode flag (0xA5 bit0). In this mode the firmware puts the whole PMIC to sleep (AXP853T sleep state, only the DRAM rails stay up) after Linux freezes — nothing on the SoC can wake it, so the EC becomes the wake authority: on its RTC alarm, the suspend deadline, or a power-button press it restarts the power-on sequence, whose PWRON escalation is exactly the AXP sleep wake source (POK negative edge). The PMIC then restores the recorded rail configuration and the SoC boots through its power-on reset. Co-Authored-By: Claude Fable 5 --- include/linux-power-control.h | 1 + include/regmap-structs.h | 1 + include/rtc-alarm-subsystem.h | 1 + src/linux-power-control.c | 17 +++++++ src/rtc-alarm-subsystem.c | 11 +++++ src/wbec.c | 45 ++++++++++++++++++- .../wbec-integration/wbec_integration_test.c | 8 ++++ unittests/wbec/wbec_test_stubs.c | 9 ++++ 8 files changed, 92 insertions(+), 1 deletion(-) diff --git a/include/linux-power-control.h b/include/linux-power-control.h index f2f1f22..253083f 100644 --- a/include/linux-power-control.h +++ b/include/linux-power-control.h @@ -5,6 +5,7 @@ void linux_cpu_pwr_seq_init(bool on); void linux_cpu_pwr_seq_off_and_goto_standby(uint16_t wakeup_after_s); void linux_cpu_pwr_seq_on(void); +void linux_cpu_pwr_seq_wakeup(void); void linux_cpu_pwr_seq_hard_off(void); void linux_cpu_pwr_seq_hard_reset(void); void linux_cpu_pwr_seq_reset_pmic(void); diff --git a/include/regmap-structs.h b/include/regmap-structs.h index 2a69272..92222ae 100644 --- a/include/regmap-structs.h +++ b/include/regmap-structs.h @@ -99,6 +99,7 @@ /* Addr Name RO/RW */ \ m( 0xA4, SUSPEND_CTRL, RW, \ /* 0xA4 */ uint16_t timeout_s; \ + /* 0xA5 */ uint16_t off_mode : 1; \ ) \ /* Addr Name RO/RW */ \ m( 0xB0, IRQ_FLAGS, RO, \ diff --git a/include/rtc-alarm-subsystem.h b/include/rtc-alarm-subsystem.h index a530be1..3724687 100644 --- a/include/rtc-alarm-subsystem.h +++ b/include/rtc-alarm-subsystem.h @@ -1,4 +1,5 @@ #pragma once bool rtc_alarm_is_alarm_enabled(void); +bool rtc_alarm_take_fired(void); void rtc_alarm_do_periodic_work(void); diff --git a/src/linux-power-control.c b/src/linux-power-control.c index 7e4b847..36d175c 100644 --- a/src/linux-power-control.c +++ b/src/linux-power-control.c @@ -145,6 +145,23 @@ void linux_cpu_pwr_seq_on(void) new_state(PS_ON_STEP1_WAIT_3V3); } +/** + * @brief Пробуждение PMIC из сна (AXP sleep, режим suspend-to-off). + * 5В уже включено, 3.3В выключил сам PMIC по команде из BL31. + * Перезапускаем последовательность включения с шага ожидания 3.3В: + * если оно не появляется само, штатная эскалация "нажимает" PWRON - + * для AXP853T в состоянии sleep это источник пробуждения (POK + * negedge), по которому PMIC восстанавливает записанную конфигурацию. + * Без сброса состояния: PS_ON_COMPLETE блокирует обычный + * linux_cpu_pwr_seq_on(). + */ +void linux_cpu_pwr_seq_wakeup(void) +{ + pmic_reset_gpio_off(); + linux_cpu_pwr_5v_gpio_on(); + new_state(PS_ON_STEP1_WAIT_3V3); +} + /** * @brief Выключение питания путём отключения 5В сразу, без PMIC. * Нужно для отключения по долгому нажатию diff --git a/src/rtc-alarm-subsystem.c b/src/rtc-alarm-subsystem.c index ffb5c22..cbc4b65 100644 --- a/src/rtc-alarm-subsystem.c +++ b/src/rtc-alarm-subsystem.c @@ -10,12 +10,22 @@ */ static bool alarm_enabled = 0; +static bool alarm_fired_latch = 0; bool rtc_alarm_is_alarm_enabled(void) { return alarm_enabled; } +// Возвращает true один раз после срабатывания будильника. +// Используется режимом suspend-to-off для пробуждения PMIC. +bool rtc_alarm_take_fired(void) +{ + bool ret = alarm_fired_latch; + alarm_fired_latch = false; + return ret; +} + void rtc_alarm_do_periodic_work(void) { if (rtc_get_ready_read()) { @@ -46,6 +56,7 @@ void rtc_alarm_do_periodic_work(void) regmap_set_region_data(REGMAP_REGION_RTC_CFG, &cfg, sizeof(cfg)); if (rtc_alarm.flag) { + alarm_fired_latch = true; irq_set_flag(IRQ_ALARM); rtc_clear_alarm_flag(); // После сработки будильника нужно его выключить diff --git a/src/wbec.c b/src/wbec.c index 88e9153..74d9bee 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -95,6 +95,10 @@ struct wbec_ctx { // между записью в SUSPEND_CTRL и заморозкой системы демон // watchdog продолжает кормить ещё ~1-2 секунды. bool suspend_started; + // Режим suspend-to-off: BL31 усыпляет PMIC целиком (остаётся + // только питание DRAM). Пробуждение делает EC: по будильнику + // (или дедлайну/кнопке) перезапускает PMIC импульсом PWRON. + bool suspend_off_mode; }; static struct wbec_ctx wbec_ctx; @@ -512,10 +516,15 @@ void wbec_do_periodic_work(void) if (s.timeout_s > 0) { wbec_ctx.suspend_mode = true; wbec_ctx.suspend_started = false; + wbec_ctx.suspend_off_mode = s.off_mode; wbec_ctx.suspend_entry_timestamp = systick_get_system_time_ms(); // Запас 10 с поверх запрошенной длительности wbec_ctx.suspend_timeout_ms = (uint32_t)s.timeout_s * 1000 + 10000; - console_print_w_prefix("Suspend mode: on\r\n"); + if (wbec_ctx.suspend_off_mode) { + console_print_w_prefix("Suspend mode: on (power-off, wake by alarm)\r\n"); + } else { + console_print_w_prefix("Suspend mode: on\r\n"); + } } else if (wbec_ctx.suspend_mode) { wbec_ctx.suspend_mode = false; console_print_w_prefix("Suspend mode: off (request)\r\n"); @@ -614,6 +623,40 @@ void wbec_do_periodic_work(void) break; } + // Suspend-to-off: питание SoC выключено самим PMIC (кроме + // DRAM), Linux заморожен в самообновляющейся памяти. Будим + // PMIC по будильнику, дедлайну или кнопке: перезапуск + // последовательности включения "нажимает" PWRON, PMIC + // восстанавливает записанную конфигурацию питания. + if (wbec_ctx.suspend_mode && wbec_ctx.suspend_off_mode && + wbec_ctx.suspend_started) { + bool alarm = rtc_alarm_take_fired(); + bool deadline = systick_get_time_since_timestamp( + wbec_ctx.suspend_entry_timestamp) > wbec_ctx.suspend_timeout_ms; + bool button = pwrkey_handle_short_press(); + + if (alarm || deadline || button) { + wbec_ctx.suspend_mode = false; + wbec_ctx.suspend_off_mode = false; + if (alarm) { + wbec_ctx.poweron_reason = REASON_RTC_ALARM; + } else if (button) { + wbec_ctx.poweron_reason = REASON_POWER_KEY; + } else { + wbec_ctx.poweron_reason = REASON_WATCHDOG; + } + console_print("\r\n\n"); + console_print_w_prefix("Suspend-to-off: wake up, restart PMIC via PWRON\r\n"); + linux_cpu_pwr_seq_wakeup(); + new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); + break; + } + // Кормления watchdog в этом режиме невозможны (Linux + // выключен) - потребляем возможный таймаут вхолостую + (void)wdt_handle_timed_out(); + break; + } + #if defined(WBEC_HAS_WARM_RESET) // Если Linux сбрасывает watchdog - система жива, // разрешаем следующую попытку тёплого сброса. diff --git a/unittests/wbec-integration/wbec_integration_test.c b/unittests/wbec-integration/wbec_integration_test.c index 898ef93..65358ac 100644 --- a/unittests/wbec-integration/wbec_integration_test.c +++ b/unittests/wbec-integration/wbec_integration_test.c @@ -124,9 +124,17 @@ void linux_poweron_handler(void) // ==================== Локальные заглушки wbec ==================== static bool alarm_enabled; +static bool alarm_fired; bool rtc_alarm_is_alarm_enabled(void) { return alarm_enabled; } +bool rtc_alarm_take_fired(void) +{ + bool ret = alarm_fired; + alarm_fired = false; + return ret; +} + bool temperature_control_is_temperature_ready(void) { return true; } int16_t temperature_control_get_temperature_c_x100(void) { return 2500; } diff --git a/unittests/wbec/wbec_test_stubs.c b/unittests/wbec/wbec_test_stubs.c index f8a9ff6..889d23b 100644 --- a/unittests/wbec/wbec_test_stubs.c +++ b/unittests/wbec/wbec_test_stubs.c @@ -163,6 +163,15 @@ void utest_rtc_alarm_set_enabled(bool enabled) alarm_enabled = enabled; } +bool rtc_alarm_take_fired(void) +{ + return false; +} + +void linux_cpu_pwr_seq_wakeup(void) +{ +} + bool rtc_alarm_is_alarm_enabled(void) { return alarm_enabled; From a882905a5da806eb007b043acdbc674becadc389 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Sun, 5 Jul 2026 23:53:02 +0300 Subject: [PATCH 06/12] suspend mode: exit quietly when the deadline passes without a started sleep An announced window whose suspend never actually happened (canceled or failed before the power cut) used to warm-reset a perfectly alive system at the deadline. If 3.3V never dropped, just leave the mode. Co-Authored-By: Claude Fable 5 --- src/wbec.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/wbec.c b/src/wbec.c index 74d9bee..8019602 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -682,6 +682,13 @@ void wbec_do_periodic_work(void) if (systick_get_time_since_timestamp(wbec_ctx.suspend_entry_timestamp) > wbec_ctx.suspend_timeout_ms) { wbec_ctx.suspend_mode = false; + if (!wbec_ctx.suspend_started) { + // Сон так и не начался (suspend отменён или не + // дошёл до отключения питания) - система живёт, + // просто тихо выходим из режима + console_print_w_prefix("Suspend mode: off (never started)\r\n"); + break; + } wbec_ctx.wd_warm_reset_attempted = true; wbec_ctx.poweron_reason = REASON_WATCHDOG_WARM; console_print("\r\n\n"); @@ -729,6 +736,10 @@ void wbec_do_periodic_work(void) if (systick_get_time_since_timestamp(wbec_ctx.suspend_entry_timestamp) > wbec_ctx.suspend_timeout_ms) { wbec_ctx.suspend_mode = false; + if (!wbec_ctx.suspend_started) { + console_print_w_prefix("Suspend mode: off (never started)\r\n"); + break; + } wbec_ctx.poweron_reason = REASON_WATCHDOG; console_print("\r\n\n"); console_print_w_prefix("Suspend deadline passed, system did not wake up - reset power.\r\n"); From 871c16906114e6d0306194d431266d4fb691a17b Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Mon, 6 Jul 2026 00:12:42 +0300 Subject: [PATCH 07/12] suspend-to-off: pulse PWROK after the PMIC wake restores power MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The AXP853T sleep-exit restores the recorded rail configuration but produces no reset edge, and the SoC does not start on its own when its rails return — the board sat with all rails up and the SoC dead until the watchdog recovery pulsed PWROK two minutes later. Do that pulse as part of the wake: once 3.3V is back, route the power-on sequence through the existing warm-reset pulse state instead of completing directly. Co-Authored-By: Claude Fable 5 --- src/linux-power-control.c | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/linux-power-control.c b/src/linux-power-control.c index 36d175c..6678f6c 100644 --- a/src/linux-power-control.c +++ b/src/linux-power-control.c @@ -35,6 +35,10 @@ enum pwr_state { }; struct pwr_ctx { + // Пробуждение из suspend-to-off: после появления 3.3В нужен + // импульс на PWROK - PMIC при выходе из сна восстанавливает + // питание, но не выдаёт сброс, и SoC сам не стартует + bool wake_pending; enum pwr_state state; systime_t timestamp; unsigned attempt; @@ -157,6 +161,7 @@ void linux_cpu_pwr_seq_on(void) */ void linux_cpu_pwr_seq_wakeup(void) { + pwr_ctx.wake_pending = true; pmic_reset_gpio_off(); linux_cpu_pwr_5v_gpio_on(); new_state(PS_ON_STEP1_WAIT_3V3); @@ -284,6 +289,14 @@ void linux_cpu_pwr_seq_do_periodic_work(void) // Первый шаг включения питания: проверка, что 3.3В появилось, после того как подали 5В case PS_ON_STEP1_WAIT_3V3: if (vmon_get_ch_status(VMON_CHANNEL_V33)) { + if (pwr_ctx.wake_pending) { + // Питание восстановлено после сна PMIC: SoC ещё в + // сбросе, толкаем его импульсом на PWROK + pwr_ctx.wake_pending = false; + pmic_reset_gpio_on(); + new_state(PS_WARM_RESET_PULSE); + break; + } // Если 3.3В появилось, то считаем что питание включено new_state(PS_ON_COMPLETE); } @@ -302,8 +315,13 @@ void linux_cpu_pwr_seq_do_periodic_work(void) // PMIC должен включаться сам после подачи 5В case PS_ON_STEP2_PMIC_PWRON: if (vmon_get_ch_status(VMON_CHANNEL_V33)) { - // Если 3.3В pmic_pwron_gpio_off(); + if (pwr_ctx.wake_pending) { + pwr_ctx.wake_pending = false; + pmic_reset_gpio_on(); + new_state(PS_WARM_RESET_PULSE); + break; + } new_state(PS_ON_COMPLETE); } if (in_state_time_ms() > 1500) { From 6fdf7993e7b8b81beac6b83cba6deb9c2dd90c9a Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Mon, 6 Jul 2026 18:26:15 +0300 Subject: [PATCH 08/12] wbec: TEMP: silence working-state beep for bench soak Comment out the power-on buzzer_beep in the WBEC_STATE_WORKING entry so the suspend-to-off bench soak (many recovery/reboot cycles) is not accompanied by a beep on every boot. Temporary, requested for the soak only: revert this commit and re-flash the EC to restore the beep. Co-Authored-By: Claude Fable 5 --- src/wbec.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/wbec.c b/src/wbec.c index 8019602..ed37657 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -120,7 +120,9 @@ static void new_state(enum wbec_state s) case WBEC_STATE_WORKING: system_led_blink(500, 1000); - buzzer_beep(EC_BUZZER_BEEP_FREQ, EC_BUZZER_BEEP_POWERON_MS); + // Временно отключено на время отладки suspend-to-off: + // каждый цикл восстановления пищит + // buzzer_beep(EC_BUZZER_BEEP_FREQ, EC_BUZZER_BEEP_POWERON_MS); linux_poweron_handler(); break; } From 89947fd524811a3d3077c2988a3dfde35c9402ef Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Tue, 7 Jul 2026 00:59:56 +0300 Subject: [PATCH 09/12] suspend-to-off: drain the stale RTC alarm latch on off-mode entry rtcwake -m mem -s 60 woke the board ~220 ms after it entered off-mode instead of after 60 s, even on the first cycle after boot. The off-mode wake evaluates rtc_alarm_take_fired(), which reads the software alarm_fired_latch (set from the STM32G0 RTC ALRAF flag). Both live in the backup-power domain / a firmware static and survive resets and power loss, and nothing consumes them outside off-mode. The alarm that last woke the board therefore leaves them latched, so the very first off-mode poll reads a stale match as a fresh wake and restarts the PMIC at once. The kernel-side ack of IRQ_FLAGS.RTC_ALARM (wbec.c commit 0d510d76b) clears a different latch (the host-visible irq-subsystem flags word), not alarm_fired_latch, so it never prevented this. Drain both representations when off-mode is entered: consume the software fired latch and clear the hardware ALRAF. After this the off-mode wake starts from a clean event state, so only a freshly armed alarm (or the deadline / power button) wakes the board -- rtcwake -m mem -s 60 now sleeps the full 60 s and resumes the same kernel. The wake path itself (linux_cpu_pwr_seq_wakeup) is unchanged. Add two wbec integration regression tests: a stale alarm latch must not wake the board immediately, and a fresh off-mode alarm must still wake it. Both fail without this change. Co-Authored-By: Claude Fable 5 --- src/wbec.c | 12 +++ .../wbec-integration/wbec_integration_test.c | 74 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/src/wbec.c b/src/wbec.c index ed37657..86181c0 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -523,6 +523,18 @@ void wbec_do_periodic_work(void) // Запас 10 с поверх запрошенной длительности wbec_ctx.suspend_timeout_ms = (uint32_t)s.timeout_s * 1000 + 10000; if (wbec_ctx.suspend_off_mode) { + // Сбрасываем возможное залипшее событие будильника перед + // входом в off-mode. Флаг ALRAF в RTC (домен резервного + // питания) и программная защёлка alarm_fired_latch + // переживают сброс/пропадание питания и не сбрасываются + // нигде, кроме off-mode. Будильник, разбудивший плату в + // прошлый раз, оставляет их взведёнными; без сброса первый + // же опрос off-mode прочитает это как свежее срабатывание и + // разбудит плату немедленно (~220 мс) вместо запрошенного + // времени. Сбрасываем обе защёлки, чтобы разбудил только + // свежий будильник (или дедлайн/кнопка). + rtc_alarm_take_fired(); + rtc_clear_alarm_flag(); console_print_w_prefix("Suspend mode: on (power-off, wake by alarm)\r\n"); } else { console_print_w_prefix("Suspend mode: on\r\n"); diff --git a/unittests/wbec-integration/wbec_integration_test.c b/unittests/wbec-integration/wbec_integration_test.c index 65358ac..501ff5b 100644 --- a/unittests/wbec-integration/wbec_integration_test.c +++ b/unittests/wbec-integration/wbec_integration_test.c @@ -561,6 +561,78 @@ static void test_escalation_alternates_warm_and_hard(void) } #endif +// Объявление окна suspend-to-off из Linux через регион SUSPEND_CTRL. +static void sim_announce_off_mode(uint16_t timeout_s) +{ + struct REGMAP_SUSPEND_CTRL s = { .timeout_s = timeout_s, .off_mode = 1 }; + regmap_set_region_data(REGMAP_REGION_SUSPEND_CTRL, &s, sizeof(s)); + utest_regmap_mark_region_changed(REGMAP_REGION_SUSPEND_CTRL); +} + +// Регрессия (rtcwake -m mem): устаревшая защёлка будильника, оставшаяся от +// прошлого пробуждения (ALRAF в домене резервного питания + программный +// alarm_fired_latch), НЕ должна будить плату сразу при входе в off-mode. +// До фикста первый же опрос off-mode читал это как свежее срабатывание и +// будил плату через ~220 мс вместо запрошенного времени сна. +static void test_suspend_off_stale_alarm_does_not_wake_immediately(void) +{ + sim.check_invariants = false; // off-mode: плата намеренно "не стартует" + sim.soc_feeds = true; + sim_boot_to_working(); + + uint32_t boots_before = sim.soc_boot_count; + + // Устаревшее событие будильника от прошлого пробуждения + alarm_fired = true; + + // Linux объявляет окно suspend-to-off (заведомо длинный дедлайн) + sim_announce_off_mode(3600); + sim_tick(); // вход в off-mode должен дренажировать защёлку + + TEST_ASSERT_FALSE_MESSAGE(alarm_fired, + "Off-mode entry must drain the stale software alarm-fired latch"); + TEST_ASSERT_TRUE_MESSAGE(utest_rtc_was_alarm_flag_cleared(), + "Off-mode entry must clear the hardware ALRAF flag"); + + // BL31 гасит 3.3В (в модели - как зависший PMIC: 3.3В пропало и не + // вернётся, пока EC не перезапустит PMIC пробуждением) + sim.pmic_crashed = true; + + // 5 секунд сна - без свежего будильника плата НЕ должна проснуться + sim_run_ms(5000); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_before, sim.soc_boot_count, + "Stale alarm latch must NOT wake the board immediately"); +} + +// Регрессия: свежий будильник, сработавший уже во время off-mode, обязан +// разбудить плату (тем же путём linux_cpu_pwr_seq_wakeup) - фикс не должен +// подавлять реальное пробуждение по будильнику. +static void test_suspend_off_fresh_alarm_wakes(void) +{ + sim.check_invariants = false; + sim.soc_feeds = true; + sim_boot_to_working(); + + uint32_t boots_before = sim.soc_boot_count; + + sim_announce_off_mode(3600); + sim_tick(); + sim.pmic_crashed = true; // BL31 снял 3.3В + sim_run_ms(2000); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_before, sim.soc_boot_count, + "Board must be asleep before a fresh alarm fires"); + + // Свежий будильник; PMIC при пробуждении восстанавливает 3.3В + alarm_fired = true; + sim.pmic_crashed = false; + sim_run_ms(3000); // время на последовательность пробуждения + + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_before, + "A fresh off-mode alarm must wake the board"); +} + int main(void) { UNITY_BEGIN(); @@ -572,6 +644,8 @@ int main(void) RUN_TEST(test_pmic_reset_request_racing_wdt_timeout_does_not_latch); RUN_TEST(test_pmic_crash_timing_sweep_never_wedges); RUN_TEST(test_poweroff_request_racing_wdt_timeout_stays_off); + RUN_TEST(test_suspend_off_stale_alarm_does_not_wake_immediately); + RUN_TEST(test_suspend_off_fresh_alarm_wakes); #if defined(WBEC_HAS_WARM_RESET) RUN_TEST(test_escalation_alternates_warm_and_hard); #endif From 2ac7e28c57944b74521656398cdb4a3d57acff92 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Tue, 7 Jul 2026 03:22:26 +0300 Subject: [PATCH 10/12] suspend-to-off: disarm the watchdog feed-exit on suspend-mode exit The "sleep started" flag (suspend_started) is armed once the EC sees 3.3V drop during an announced window, and from then on a watchdog feed exits suspend mode (that gate stops the daemon's 1-2 s post-announce feed from cancelling the sleep before it begins). But the flag was only ever reset on the *entry* of the next announce, never on exit -- and the off-mode wake is a same-kernel resume that does not restart the EC, so the flag survived from one cycle into the next. On the bench the first rtcwake -m mem slept, but every later one exited immediately (RF2+: [EC] Suspend mode: off (request)): entering cycle 2 with the arm already set, the watchdog daemon's feed ended suspend mode during entry. Reset suspend_started in every suspend-mode exit path -- off-mode alarm/ deadline/button wake, the watchdog-fed exit, the deadline backstop and the host de-announce (both the WARM_RESET and legacy builds) -- so the arm returns to its power-on (disarmed) state after every exit and each cycle starts clean like a fresh boot. The entry reset is kept. Add wbec integration regression tests: a fresh boot arms the flag on the 3.3V drop and a suspend-mode exit must leave it disarmed (fails without this change), and a second off-mode cycle with the watchdog feeding must still sleep and wake on its alarm rather than exit on the feed. A new __unittest_env__ accessor exposes the flag. Keeps the ALRAF-drain (89947fd) and everything else intact. Co-Authored-By: Claude Fable 5 --- src/wbec.c | 21 ++++++ .../wbec-integration/wbec_integration_test.c | 74 +++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/src/wbec.c b/src/wbec.c index 86181c0..f396074 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -541,6 +541,11 @@ void wbec_do_periodic_work(void) } } else if (wbec_ctx.suspend_mode) { wbec_ctx.suspend_mode = false; + // Разоружаем «сон начался» при выходе из режима, чтобы на + // следующем цикле кормление watchdog не завершило suspend + // немедленно (флаг живёт в ЕС между циклами - тёплый сброс + // и пробуждение по будильнику не перезапускают ЕС). + wbec_ctx.suspend_started = false; console_print_w_prefix("Suspend mode: off (request)\r\n"); } } @@ -652,6 +657,11 @@ void wbec_do_periodic_work(void) if (alarm || deadline || button) { wbec_ctx.suspend_mode = false; wbec_ctx.suspend_off_mode = false; + // Разоружаем «сон начался»: пробуждение по будильнику - это + // resume того же ядра, ЕС не перезапускается, поэтому без + // сброса флаг остаётся взведён и на следующем цикле первое + // же кормление watchdog завершит suspend немедленно. + wbec_ctx.suspend_started = false; if (alarm) { wbec_ctx.poweron_reason = REASON_RTC_ALARM; } else if (button) { @@ -683,6 +693,7 @@ void wbec_do_periodic_work(void) // демон watchdog продолжает кормить. if (wbec_ctx.suspend_mode && wbec_ctx.suspend_started) { wbec_ctx.suspend_mode = false; + wbec_ctx.suspend_started = false; console_print_w_prefix("Suspend mode: off (watchdog fed)\r\n"); } } @@ -703,6 +714,7 @@ void wbec_do_periodic_work(void) console_print_w_prefix("Suspend mode: off (never started)\r\n"); break; } + wbec_ctx.suspend_started = false; wbec_ctx.wd_warm_reset_attempted = true; wbec_ctx.poweron_reason = REASON_WATCHDOG_WARM; console_print("\r\n\n"); @@ -740,6 +752,7 @@ void wbec_do_periodic_work(void) // только после реального начала сна (3.3В пропадало) if (wdt_handle_fed() && wbec_ctx.suspend_mode && wbec_ctx.suspend_started) { wbec_ctx.suspend_mode = false; + wbec_ctx.suspend_started = false; console_print_w_prefix("Suspend mode: off (watchdog fed)\r\n"); } @@ -754,6 +767,7 @@ void wbec_do_periodic_work(void) console_print_w_prefix("Suspend mode: off (never started)\r\n"); break; } + wbec_ctx.suspend_started = false; wbec_ctx.poweron_reason = REASON_WATCHDOG; console_print("\r\n\n"); console_print_w_prefix("Suspend deadline passed, system did not wake up - reset power.\r\n"); @@ -849,4 +863,11 @@ void wbec_do_periodic_work(void) { memset(&wbec_ctx, 0, sizeof(wbec_ctx)); } + + // «Сон начался» / разрешение выхода из suspend по кормлению watchdog. + // Должен возвращаться в исходное (false) состояние после выхода из режима. + bool utest_wbec_get_suspend_started(void) + { + return wbec_ctx.suspend_started; + } #endif diff --git a/unittests/wbec-integration/wbec_integration_test.c b/unittests/wbec-integration/wbec_integration_test.c index 501ff5b..8f37805 100644 --- a/unittests/wbec-integration/wbec_integration_test.c +++ b/unittests/wbec-integration/wbec_integration_test.c @@ -311,6 +311,7 @@ static void sim_run_ms(uint32_t duration_ms) // ==================== Подготовка сценариев ==================== void utest_wbec_reset_state(void); +bool utest_wbec_get_suspend_started(void); void utest_linux_power_control_reset_state(void); void utest_wdt_module_reset_state(void); @@ -633,6 +634,77 @@ static void test_suspend_off_fresh_alarm_wakes(void) "A fresh off-mode alarm must wake the board"); } +// Один цикл off-mode: объявление, пропадание 3.3В (сон начался), +// пробуждение по свежему будильнику (resume того же ядра), возврат в WORKING. +static void sim_off_mode_cycle(void) +{ + sim_announce_off_mode(60); + sim_tick(); + sim.pmic_crashed = true; // BL31 снял 3.3В -> arm взводится + sim_run_ms(500); + alarm_fired = true; // будильник будит + sim.pmic_crashed = false; + sim_run_ms(3000); // последовательность пробуждения -> WORKING +} + +// Регрессия (fail-without-fix): после выхода из off-mode по будильнику (resume +// того же ядра - ЕС не перезапускается) флаг «сон начался» / разрешение выхода +// из suspend по кормлению watchdog ОБЯЗАН вернуться в исходное (disarmed) +// состояние. Иначе на следующем цикле первое же кормление watchdog завершит +// suspend немедленно (на стенде: RF2+ [EC] Suspend mode: off (request)). +static void test_suspend_off_exit_disarms_feed_exit(void) +{ + sim.check_invariants = false; + sim.soc_feeds = true; + sim_boot_to_working(); + + sim_announce_off_mode(60); + sim_tick(); + sim.pmic_crashed = true; // 3.3В пропало -> arm взводится + sim_run_ms(500); + TEST_ASSERT_TRUE_MESSAGE(utest_wbec_get_suspend_started(), + "feed-exit arm must be set once the EC has seen the 3.3V drop"); + + alarm_fired = true; // будильник будит, resume того же ядра + sim.pmic_crashed = false; + sim_run_ms(3000); + + TEST_ASSERT_FALSE_MESSAGE(utest_wbec_get_suspend_started(), + "suspend-mode exit must disarm the feed-exit / seen-3.3V-drop flag"); +} + +// Регрессия многоцикловая: цикл 1 взводит arm и выходит по будильнику; на +// цикле 2 (watchdog работает и кормит во время входа) плата обязана так же +// уснуть и проснуться по будильнику, а не выйти из suspend по кормлению. +static void test_suspend_off_second_cycle_sleeps_with_watchdog(void) +{ + sim.check_invariants = false; + sim.soc_feeds = true; + sim_boot_to_working(); + + sim_off_mode_cycle(); // цикл 1 + uint32_t boots_after_c1 = sim.soc_boot_count; + + // Цикл 2: объявляем и кормим watchdog во время входа (до и после 3.3В) + sim_announce_off_mode(60); + sim_tick(); + soc_feed_watchdog(); + sim_run_ms(150); + sim.pmic_crashed = true; // 3.3В пропало + sim_run_ms(300); + soc_feed_watchdog(); // кормление при взведённом arm + sim_run_ms(1000); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_after_c1, sim.soc_boot_count, + "cycle 2 must stay asleep, not exit suspend on a watchdog feed"); + + alarm_fired = true; // свежий будильник + sim.pmic_crashed = false; + sim_run_ms(3000); + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_after_c1, + "cycle 2 must still wake on a fresh alarm"); +} + int main(void) { UNITY_BEGIN(); @@ -646,6 +718,8 @@ int main(void) RUN_TEST(test_poweroff_request_racing_wdt_timeout_stays_off); RUN_TEST(test_suspend_off_stale_alarm_does_not_wake_immediately); RUN_TEST(test_suspend_off_fresh_alarm_wakes); + RUN_TEST(test_suspend_off_exit_disarms_feed_exit); + RUN_TEST(test_suspend_off_second_cycle_sleeps_with_watchdog); #if defined(WBEC_HAS_WARM_RESET) RUN_TEST(test_escalation_alternates_warm_and_hard); #endif From f3face867609e166ce4d094e3495fbfae5aa8d4c Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Tue, 7 Jul 2026 04:10:42 +0300 Subject: [PATCH 11/12] Revert "wbec: TEMP: silence working-state beep for bench soak" This reverts commit 6fdf7993e7b8b81beac6b83cba6deb9c2dd90c9a. The working-state power-on beep was commented out only to keep the suspend-to-off bench soak quiet across its many recovery/reboot cycles; that commit was marked revert-before-merge. The soak work is done, so restore the stock beep on WBEC_STATE_WORKING entry for production. The buzzer_beep() line is byte-identical to its pre-6fdf799 form; the suspend-to-off fixes (89947fd ALRAF drain, 2ac7e28 exit-arm reset) are untouched. Co-Authored-By: Claude Fable 5 --- src/wbec.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/wbec.c b/src/wbec.c index f396074..a0cf8f9 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -120,9 +120,7 @@ static void new_state(enum wbec_state s) case WBEC_STATE_WORKING: system_led_blink(500, 1000); - // Временно отключено на время отладки suspend-to-off: - // каждый цикл восстановления пищит - // buzzer_beep(EC_BUZZER_BEEP_FREQ, EC_BUZZER_BEEP_POWERON_MS); + buzzer_beep(EC_BUZZER_BEEP_FREQ, EC_BUZZER_BEEP_POWERON_MS); linux_poweron_handler(); break; } From a6551287c67f567a6f768375cd562fa8180590fc Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Tue, 7 Jul 2026 16:24:06 +0300 Subject: [PATCH 12/12] wbec: gate power-on beep to skip suspend-to-off resume MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The working-state beep (buzzer_beep in new_state(WBEC_STATE_WORKING)) fired on every entry to WORKING, including the suspend-to-off resume. On resume the SoC re-initializes DRAM over self-refresh (content- preserving) — a path sensitive to power/timing perturbation. The buzzer firing at that moment corrupts the resume and the board cold- boots instead of waking (hw-proven 2026-07-07: beep-on-resume -> cold boot; gated -> clean resume). Gate the beep with a suspend_resume_no_beep flag set on the suspend-to- off wake path. The board still beeps on a genuine power-up (cold start / reboot — which init DRAM from scratch and are insensitive) but stays silent on resume. This supersedes the naive f3face8 "restore beep" which reintroduced the cold-boot; the earlier "TEMP silence" was masking this timing bug. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Qj3VnuyyfUAWs7qFKvVZX9 --- src/wbec.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/wbec.c b/src/wbec.c index a0cf8f9..d000f8f 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -99,6 +99,17 @@ struct wbec_ctx { // только питание DRAM). Пробуждение делает EC: по будильнику // (или дедлайну/кнопке) перезапускает PMIC импульсом PWRON. bool suspend_off_mode; + // Взводится при пробуждении из suspend-to-off, чтобы подавить + // звуковой сигнал включения на этом входе в WBEC_STATE_WORKING. + // ПОЧЕМУ это критично: resume из suspend-to-off — это ре-инициализация + // DRAM поверх самообновления (содержимое памяти сохраняется), процесс + // очень чувствителен к возмущениям по питанию/таймингу. Пищалка, + // сработавшая в этот момент, срывает resume, и плата уходит в + // холодную перезагрузку вместо пробуждения (проверено на железе + // 2026-07-07: включённый бип на resume → cold-boot; выключенный → ок). + // Обычное включение (холодный старт/перезагрузка) инициализирует DRAM + // с нуля и к сигналу не чувствительно — там бип нужен и безопасен. + bool suspend_resume_no_beep; }; static struct wbec_ctx wbec_ctx; @@ -120,7 +131,14 @@ static void new_state(enum wbec_state s) case WBEC_STATE_WORKING: system_led_blink(500, 1000); - buzzer_beep(EC_BUZZER_BEEP_FREQ, EC_BUZZER_BEEP_POWERON_MS); + // Сигнал включения — на РЕАЛЬНОМ включении (холодный старт/перезагрузка), + // но НЕ на resume из suspend-to-off: там пищалка возмущает ре-инициализацию + // DRAM поверх самообновления и плата уходит в cold-boot вместо пробуждения + // (проверено на железе). Флаг взводится на пути пробуждения suspend-to-off. + if (!wbec_ctx.suspend_resume_no_beep) { + buzzer_beep(EC_BUZZER_BEEP_FREQ, EC_BUZZER_BEEP_POWERON_MS); + } + wbec_ctx.suspend_resume_no_beep = false; linux_poweron_handler(); break; } @@ -660,6 +678,11 @@ void wbec_do_periodic_work(void) // сброса флаг остаётся взведён и на следующем цикле первое // же кормление watchdog завершит suspend немедленно. wbec_ctx.suspend_started = false; + // Это resume того же ядра (DRAM в самообновлении): бип на + // входе в WORKING сорвёт ре-инициализацию DRAM и уронит плату + // в cold-boot. Подавляем сигнал включения именно для этого + // пробуждения (см. suspend_resume_no_beep в new_state()). + wbec_ctx.suspend_resume_no_beep = true; if (alarm) { wbec_ctx.poweron_reason = REASON_RTC_ALARM; } else if (button) {