diff --git a/debian/changelog b/debian/changelog index 31147c7..b46d29b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,36 @@ +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) + * 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 + 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..7e4b847 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 { @@ -136,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); } @@ -148,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); } @@ -159,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); } @@ -173,6 +185,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 +322,21 @@ 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(); + // Тёплый сброс всегда заканчивается последовательностью включения. + // Гарантируем ей 5В: если тёплый сброс был запрошен, когда 5В + // оказалось снятым, ожидание 3.3В без 5В бессмысленно + linux_cpu_pwr_5v_gpio_on(); + 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..3b89dc0 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,68 @@ 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 } + // За один проход разрешено только одно действие с питанием. + // Если запрос из 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 - система жива, + // разрешаем следующую попытку тёплого сброса + 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(); + // Жёсткий сброс - последняя ступень эскалации. После него + // начинается новый цикл загрузки, и первое зависание в нём + // снова заслуживает тёплого сброса (с сохранением DRAM/ramoops), + // поэтому эскалация начинается заново + wbec_ctx.wd_warm_reset_attempted = false; + } + new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); + } +#else // Если сработал WDT - перезагружаемся по питанию if (wdt_handle_timed_out()) { wbec_ctx.poweron_reason = REASON_WATCHDOG; @@ -561,6 +623,14 @@ void wbec_do_periodic_work(void) linux_cpu_pwr_seq_hard_reset(); new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); } +#endif + + // Сработал watchdog - сброс уже начат. Контроль 3.3В в этом проходе + // пропускаем, чтобы не запустить вторую последовательность поверх + // начатой (см. комментарий выше) + if (wbec_ctx.state != WBEC_STATE_WORKING) { + break; + } // Если пропало 3.3В - пробуем перезапустить питание, но не более N раз за M минут // Если питание пропадает слишком часто - выключаемся diff --git a/src/wdt.c b/src/wdt.c index c128d49..03796e9 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,27 @@ 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; +} + +#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) { @@ -78,6 +100,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..5c4a942 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. @@ -907,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-линия поднимается сразу. @@ -978,6 +1131,11 @@ 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_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/regmap_test b/unittests/regmap_test new file mode 100755 index 0000000..98c1658 Binary files /dev/null and b/unittests/regmap_test differ diff --git a/unittests/utest_helpers/wdt/utest_wdt.c b/unittests/utest_helpers/wdt/utest_wdt.c index 8aafc8c..c4c2dcb 100644 --- a/unittests/utest_helpers/wdt/utest_wdt.c +++ b/unittests/utest_helpers/wdt/utest_wdt.c @@ -6,6 +6,7 @@ static struct { uint16_t timeout; bool started; bool timed_out; + bool fed; } wdt_state = {0}; void utest_wdt_reset(void) @@ -18,6 +19,11 @@ void utest_wdt_set_timed_out(bool value) wdt_state.timed_out = value; } +void utest_wdt_set_fed(bool value) +{ + wdt_state.fed = value; +} + uint16_t utest_wdt_get_timeout(void) { return wdt_state.timeout; @@ -50,4 +56,11 @@ bool wdt_handle_timed_out(void) return ret; } +bool wdt_handle_fed(void) +{ + bool ret = wdt_state.fed; + wdt_state.fed = false; + return ret; +} + void wdt_do_periodic_work(void) {} diff --git a/unittests/utest_helpers/wdt/utest_wdt.h b/unittests/utest_helpers/wdt/utest_wdt.h index ebe5f80..8cad37f 100644 --- a/unittests/utest_helpers/wdt/utest_wdt.h +++ b/unittests/utest_helpers/wdt/utest_wdt.h @@ -5,5 +5,6 @@ void utest_wdt_reset(void); void utest_wdt_set_timed_out(bool value); +void utest_wdt_set_fed(bool value); uint16_t utest_wdt_get_timeout(void); bool utest_wdt_get_started(void); 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 04d36f5..1ff8637 100644 --- a/unittests/wbec/wbec_test.c +++ b/unittests/wbec/wbec_test.c @@ -32,6 +32,7 @@ enum utest_wbec_poweron_reason { UTEST_REASON_WATCHDOG, UTEST_REASON_PMIC_OFF, UTEST_REASON_UNKNOWN, + UTEST_REASON_WATCHDOG_WARM, }; static void reset_all(void) @@ -986,7 +987,9 @@ static void test_periodic_working_reboot_request(void) "poweron_reason must be REASON_REBOOT"); } -// Сценарий: запрос pmic_reset из Linux → reset_pmic. +// Сценарий: запрос pmic_reset из Linux. +// На моделях с WBEC_HAS_WARM_RESET → тёплый сброс (warm_reset), +// на остальных → историческое поведение (reset_pmic, удержание до 2 с). static void test_periodic_working_pmic_reset_request(void) { drive_to_working_state(); @@ -994,14 +997,30 @@ static void test_periodic_working_pmic_reset_request(void) set_power_ctrl_request(false, false, true); 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 pmic_reset request"); +#else TEST_ASSERT_TRUE_MESSAGE(utest_linux_pwr_get_reset_pmic_called(), "reset_pmic must be called on pmic_reset request"); + TEST_ASSERT_FALSE_MESSAGE(utest_linux_pwr_get_warm_reset_called(), + "warm_reset must NOT be called on models without warm reset support"); +#endif TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_REBOOT, get_poweron_reason_from_regmap(), "poweron_reason must be REASON_REBOOT"); } -// Сценарий: WDT тайм-аут → hard_reset, reason=WATCHDOG. +// Вспомогательная: возврат из POWER_ON_SEQUENCE_WAIT в WORKING после сброса +static void drive_back_to_working_state(void) +{ + // POWER_ON_SEQUENCE_WAIT → WORKING (is_busy=false по умолчанию) + wbec_do_periodic_work(); +} + +// Сценарий: первый WDT тайм-аут. +// На моделях с WBEC_HAS_WARM_RESET → warm_reset (DRAM сохраняется), reason=WATCHDOG_WARM. +// На остальных → сразу жёсткий сброс по питанию, reason=WATCHDOG (историческое поведение). static void test_periodic_working_wdt_timeout(void) { drive_to_working_state(); @@ -1009,13 +1028,209 @@ static void test_periodic_working_wdt_timeout(void) utest_wdt_set_timed_out(true); 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 first WDT timeout"); + TEST_ASSERT_FALSE_MESSAGE(utest_linux_pwr_get_hard_reset_called(), + "hard_reset must NOT be called on first WDT timeout"); + + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_WATCHDOG_WARM, get_poweron_reason_from_regmap(), + "poweron_reason must be REASON_WATCHDOG_WARM"); +#else TEST_ASSERT_TRUE_MESSAGE(utest_linux_pwr_get_hard_reset_called(), "hard_reset must be called on WDT timeout"); + TEST_ASSERT_FALSE_MESSAGE(utest_linux_pwr_get_warm_reset_called(), + "warm_reset must NOT be called on models without warm reset support"); TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_WATCHDOG, get_poweron_reason_from_regmap(), "poweron_reason must be REASON_WATCHDOG"); +#endif +} + +#if defined(WBEC_HAS_WARM_RESET) +// Сценарий: повторный WDT тайм-аут без сброса watchdog из Linux → hard_reset, reason=WATCHDOG. +static void test_periodic_working_wdt_timeout_second_time_hard(void) +{ + drive_to_working_state(); + + // Первый тайм-аут → warm_reset + 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"); + + // Linux не ожил: watchdog не сбрасывался, второй тайм-аут + 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 without feed"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_WATCHDOG, get_poweron_reason_from_regmap(), + "poweron_reason must be REASON_WATCHDOG"); +} + +// Сценарий: после warm_reset Linux ожил (сбросил watchdog) → следующий тайм-аут снова warm_reset. +static void test_periodic_working_wdt_feed_resets_escalation(void) +{ + drive_to_working_state(); + + // Первый тайм-аут → warm_reset + 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(); + + // Linux ожил и сбросил watchdog + drive_back_to_working_state(); + utest_wdt_set_fed(true); + wbec_do_periodic_work(); + + // Новый тайм-аут → снова warm_reset, эскалация сброшена + 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 after watchdog was fed"); + TEST_ASSERT_FALSE_MESSAGE(utest_linux_pwr_get_hard_reset_called(), + "hard_reset must NOT be called after watchdog was fed"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_WATCHDOG_WARM, get_poweron_reason_from_regmap(), + "poweron_reason must be REASON_WATCHDOG_WARM"); +} +#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) { @@ -1235,6 +1450,15 @@ int main(void) RUN_TEST(test_periodic_working_reboot_request); RUN_TEST(test_periodic_working_pmic_reset_request); RUN_TEST(test_periodic_working_wdt_timeout); +#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); diff --git a/unittests/wbec/wbec_test_stubs.c b/unittests/wbec/wbec_test_stubs.c index 2b7620b..f8a9ff6 100644 --- a/unittests/wbec/wbec_test_stubs.c +++ b/unittests/wbec/wbec_test_stubs.c @@ -10,6 +10,7 @@ static struct { bool hard_off_called; bool hard_reset_called; bool reset_pmic_called; + bool warm_reset_called; bool is_busy; bool standby_called; uint16_t standby_wakeup_s; @@ -106,6 +107,21 @@ void linux_cpu_pwr_seq_reset_pmic(void) linux_pwr_state.reset_pmic_called = true; } +void linux_cpu_pwr_seq_warm_reset(void) +{ + linux_pwr_state.warm_reset_called = true; +} + +bool utest_linux_pwr_get_warm_reset_called(void) +{ + return linux_pwr_state.warm_reset_called; +} + +void utest_linux_pwr_clear_warm_reset_called(void) +{ + linux_pwr_state.warm_reset_called = false; +} + bool linux_cpu_pwr_seq_is_busy(void) { return linux_pwr_state.is_busy; diff --git a/unittests/wbec/wbec_test_stubs.h b/unittests/wbec/wbec_test_stubs.h index 1ee62eb..18aacca 100644 --- a/unittests/wbec/wbec_test_stubs.h +++ b/unittests/wbec/wbec_test_stubs.h @@ -17,6 +17,8 @@ bool utest_linux_pwr_get_pwr_on_called(void); bool utest_linux_pwr_get_hard_off_called(void); bool utest_linux_pwr_get_hard_reset_called(void); bool utest_linux_pwr_get_reset_pmic_called(void); +bool utest_linux_pwr_get_warm_reset_called(void); +void utest_linux_pwr_clear_warm_reset_called(void); void utest_temp_set_ready(bool ready); void utest_rtc_alarm_reset(void); void utest_rtc_alarm_set_enabled(bool enabled); diff --git a/unittests/wdt/wdt_test.c b/unittests/wdt/wdt_test.c index 8a5bc4e..99e4c90 100644 --- a/unittests/wdt/wdt_test.c +++ b/unittests/wdt/wdt_test.c @@ -15,8 +15,9 @@ void setUp(void) utest_systick_set_time_ms(1000); utest_regmap_reset(); - // Сброс флага timed_out + // Сброс флагов timed_out и fed wdt_handle_timed_out(); + wdt_handle_fed(); } void tearDown(void) @@ -388,6 +389,50 @@ static void test_wdt_regmap_reset_command(void) "Watchdog should timeout after period from reset"); } +// Сценарий: Сброс watchdog через regmap выставляет признак fed (система жива) +// Ожидается: wdt_handle_fed() возвращает true один раз после сброса через regmap +// и false после чтения; сброс из прошивки (wdt_start_reset) признак не выставляет +static void test_wdt_regmap_reset_sets_fed_flag(void) +{ + LOG_INFO("Testing fed flag after watchdog reset via regmap"); + + wdt_set_timeout(5); + wdt_start_reset(); + wdt_do_periodic_work(); + + // Сброс из прошивки не считается "кормлением" из Linux + TEST_ASSERT_FALSE_MESSAGE(wdt_handle_fed(), + "Fed flag must not be set by firmware-initiated reset"); + + // Отправляем команду сброса через regmap + struct REGMAP_WDT w = { + .timeout = 5, + .reset = 1 + }; + TEST_ASSERT_TRUE_MESSAGE(regmap_set_region_data(REGMAP_REGION_WDT, &w, sizeof(w)), + "Failed to set WDT regmap data"); + utest_regmap_mark_region_changed(REGMAP_REGION_WDT); + wdt_do_periodic_work(); + + TEST_ASSERT_TRUE_MESSAGE(wdt_handle_fed(), + "Fed flag must be set after reset via regmap"); + TEST_ASSERT_FALSE_MESSAGE(wdt_handle_fed(), + "Fed flag must be cleared after reading"); + + // Запись в regmap только нового таймаута (без флага reset) перезапускает + // таймер, но НЕ считается "кормлением" из Linux: эскалация тёплого + // сброса не должна сбрасываться такой записью + w.timeout = 7; + w.reset = 0; + TEST_ASSERT_TRUE_MESSAGE(regmap_set_region_data(REGMAP_REGION_WDT, &w, sizeof(w)), + "Failed to set WDT regmap data"); + utest_regmap_mark_region_changed(REGMAP_REGION_WDT); + wdt_do_periodic_work(); + + TEST_ASSERT_FALSE_MESSAGE(wdt_handle_fed(), + "Fed flag must not be set by a timeout-only regmap write"); +} + // Сценарий: Изменение таймаута с 10с на 3с И установка флага сброса одновременно // Ожидается: Обе операции применены, таймаут обновлён до 3с, флаг сброса очищен, // watchdog сброшен; применяется новый период таймаута @@ -585,6 +630,7 @@ int main(void) RUN_TEST(test_wdt_regmap_timeout_change); RUN_TEST(test_wdt_regmap_timeout_decrease_prevents_false_trigger); RUN_TEST(test_wdt_regmap_reset_command); + RUN_TEST(test_wdt_regmap_reset_sets_fed_flag); RUN_TEST(test_wdt_regmap_timeout_and_reset_simultaneous); RUN_TEST(test_wdt_regmap_timeout_bounds_via_regmap); RUN_TEST(test_wdt_regmap_no_change);