From de9b31afcbfc40fdf4d62bd44a08f63a70430363 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Sat, 4 Jul 2026 04:05:24 +0300 Subject: [PATCH 1/2] 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 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 Date: Sat, 4 Jul 2026 21:58:32 +0300 Subject: [PATCH 2/2] 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);