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..253083f 100644 --- a/include/linux-power-control.h +++ b/include/linux-power-control.h @@ -5,8 +5,10 @@ void linux_cpu_pwr_seq_init(bool on); void linux_cpu_pwr_seq_off_and_goto_standby(uint16_t wakeup_after_s); void linux_cpu_pwr_seq_on(void); +void linux_cpu_pwr_seq_wakeup(void); void linux_cpu_pwr_seq_hard_off(void); void linux_cpu_pwr_seq_hard_reset(void); void linux_cpu_pwr_seq_reset_pmic(void); +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/regmap-structs.h b/include/regmap-structs.h index b95fa49..92222ae 100644 --- a/include/regmap-structs.h +++ b/include/regmap-structs.h @@ -97,6 +97,11 @@ /* -//- */ uint16_t reset_pmic : 1; \ ) \ /* Addr Name RO/RW */ \ + m( 0xA4, SUSPEND_CTRL, RW, \ + /* 0xA4 */ uint16_t timeout_s; \ + /* 0xA5 */ uint16_t off_mode : 1; \ + ) \ + /* Addr Name RO/RW */ \ m( 0xB0, IRQ_FLAGS, RO, \ /* 0xB0 */ uint16_t irqs; \ ) \ diff --git a/include/rtc-alarm-subsystem.h b/include/rtc-alarm-subsystem.h index a530be1..3724687 100644 --- a/include/rtc-alarm-subsystem.h +++ b/include/rtc-alarm-subsystem.h @@ -1,4 +1,5 @@ #pragma once bool rtc_alarm_is_alarm_enabled(void); +bool rtc_alarm_take_fired(void); void rtc_alarm_do_periodic_work(void); diff --git a/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..6678f6c 100644 --- a/src/linux-power-control.c +++ b/src/linux-power-control.c @@ -31,9 +31,14 @@ 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 { + // Пробуждение из suspend-to-off: после появления 3.3В нужен + // импульс на PWROK - PMIC при выходе из сна восстанавливает + // питание, но не выдаёт сброс, и SoC сам не стартует + bool wake_pending; enum pwr_state state; systime_t timestamp; unsigned attempt; @@ -136,6 +141,28 @@ 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); +} + +/** + * @brief Пробуждение PMIC из сна (AXP sleep, режим suspend-to-off). + * 5В уже включено, 3.3В выключил сам PMIC по команде из BL31. + * Перезапускаем последовательность включения с шага ожидания 3.3В: + * если оно не появляется само, штатная эскалация "нажимает" PWRON - + * для AXP853T в состоянии sleep это источник пробуждения (POK + * negedge), по которому PMIC восстанавливает записанную конфигурацию. + * Без сброса состояния: PS_ON_COMPLETE блокирует обычный + * linux_cpu_pwr_seq_on(). + */ +void linux_cpu_pwr_seq_wakeup(void) +{ + pwr_ctx.wake_pending = true; + pmic_reset_gpio_off(); linux_cpu_pwr_5v_gpio_on(); new_state(PS_ON_STEP1_WAIT_3V3); } @@ -148,6 +175,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 +189,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 +207,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 Статус работы алгоритма управления питанием * @@ -239,6 +289,14 @@ void linux_cpu_pwr_seq_do_periodic_work(void) // Первый шаг включения питания: проверка, что 3.3В появилось, после того как подали 5В case PS_ON_STEP1_WAIT_3V3: if (vmon_get_ch_status(VMON_CHANNEL_V33)) { + if (pwr_ctx.wake_pending) { + // Питание восстановлено после сна PMIC: SoC ещё в + // сбросе, толкаем его импульсом на PWROK + pwr_ctx.wake_pending = false; + pmic_reset_gpio_on(); + new_state(PS_WARM_RESET_PULSE); + break; + } // Если 3.3В появилось, то считаем что питание включено new_state(PS_ON_COMPLETE); } @@ -257,8 +315,13 @@ void linux_cpu_pwr_seq_do_periodic_work(void) // PMIC должен включаться сам после подачи 5В case PS_ON_STEP2_PMIC_PWRON: if (vmon_get_ch_status(VMON_CHANNEL_V33)) { - // Если 3.3В pmic_pwron_gpio_off(); + if (pwr_ctx.wake_pending) { + pwr_ctx.wake_pending = false; + pmic_reset_gpio_on(); + new_state(PS_WARM_RESET_PULSE); + break; + } new_state(PS_ON_COMPLETE); } if (in_state_time_ms() > 1500) { @@ -294,6 +357,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/rtc-alarm-subsystem.c b/src/rtc-alarm-subsystem.c index ffb5c22..cbc4b65 100644 --- a/src/rtc-alarm-subsystem.c +++ b/src/rtc-alarm-subsystem.c @@ -10,12 +10,22 @@ */ static bool alarm_enabled = 0; +static bool alarm_fired_latch = 0; bool rtc_alarm_is_alarm_enabled(void) { return alarm_enabled; } +// Возвращает true один раз после срабатывания будильника. +// Используется режимом suspend-to-off для пробуждения PMIC. +bool rtc_alarm_take_fired(void) +{ + bool ret = alarm_fired_latch; + alarm_fired_latch = false; + return ret; +} + void rtc_alarm_do_periodic_work(void) { if (rtc_get_ready_read()) { @@ -46,6 +56,7 @@ void rtc_alarm_do_periodic_work(void) regmap_set_region_data(REGMAP_REGION_RTC_CFG, &cfg, sizeof(cfg)); if (rtc_alarm.flag) { + alarm_fired_latch = true; irq_set_flag(IRQ_ALARM); rtc_clear_alarm_flag(); // После сработки будильника нужно его выключить diff --git a/src/wbec.c b/src/wbec.c index 0f70c07..d000f8f 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,37 @@ 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; + // Режим suspend: Linux объявил окно сна через SUSPEND_CTRL. + // В этом режиме потеря 3.3В ожидаема (BL31 гасит DCDC1), а + // watchdog-эскалация заменена на дедлайн запрошенной длительности. + bool suspend_mode; + systime_t suspend_entry_timestamp; + uint32_t suspend_timeout_ms; + // Взводится, когда во время suspend реально пропало 3.3В. + // До этого момента кормления watchdog НЕ завершают режим: + // между записью в SUSPEND_CTRL и заморозкой системы демон + // watchdog продолжает кормить ещё ~1-2 секунды. + bool suspend_started; + // Режим suspend-to-off: BL31 усыпляет PMIC целиком (остаётся + // только питание DRAM). Пробуждение делает EC: по будильнику + // (или дедлайну/кнопке) перезапускает PMIC импульсом PWRON. + bool suspend_off_mode; + // Взводится при пробуждении из suspend-to-off, чтобы подавить + // звуковой сигнал включения на этом входе в WBEC_STATE_WORKING. + // ПОЧЕМУ это критично: resume из suspend-to-off — это ре-инициализация + // DRAM поверх самообновления (содержимое памяти сохраняется), процесс + // очень чувствителен к возмущениям по питанию/таймингу. Пищалка, + // сработавшая в этот момент, срывает resume, и плата уходит в + // холодную перезагрузку вместо пробуждения (проверено на железе + // 2026-07-07: включённый бип на resume → cold-boot; выключенный → ок). + // Обычное включение (холодный старт/перезагрузка) инициализирует DRAM + // с нуля и к сигналу не чувствительно — там бип нужен и безопасен. + bool suspend_resume_no_beep; }; static struct wbec_ctx wbec_ctx; @@ -98,7 +131,14 @@ static void new_state(enum wbec_state s) case WBEC_STATE_WORKING: system_led_blink(500, 1000); - buzzer_beep(EC_BUZZER_BEEP_FREQ, EC_BUZZER_BEEP_POWERON_MS); + // Сигнал включения — на РЕАЛЬНОМ включении (холодный старт/перезагрузка), + // но НЕ на resume из suspend-to-off: там пищалка возмущает ре-инициализацию + // DRAM поверх самообновления и плата уходит в cold-boot вместо пробуждения + // (проверено на железе). Флаг взводится на пути пробуждения suspend-to-off. + if (!wbec_ctx.suspend_resume_no_beep) { + buzzer_beep(EC_BUZZER_BEEP_FREQ, EC_BUZZER_BEEP_POWERON_MS); + } + wbec_ctx.suspend_resume_no_beep = false; linux_poweron_handler(); break; } @@ -486,6 +526,47 @@ void wbec_do_periodic_work(void) } } + // Запрос режима suspend из Linux: запись таймаута (секунды) в + // SUSPEND_CTRL включает режим, запись 0 - выключает. + { + struct REGMAP_SUSPEND_CTRL s; + if (regmap_get_data_if_region_changed(REGMAP_REGION_SUSPEND_CTRL, &s, sizeof(s))) { + if (s.timeout_s > 0) { + wbec_ctx.suspend_mode = true; + wbec_ctx.suspend_started = false; + wbec_ctx.suspend_off_mode = s.off_mode; + wbec_ctx.suspend_entry_timestamp = systick_get_system_time_ms(); + // Запас 10 с поверх запрошенной длительности + wbec_ctx.suspend_timeout_ms = (uint32_t)s.timeout_s * 1000 + 10000; + if (wbec_ctx.suspend_off_mode) { + // Сбрасываем возможное залипшее событие будильника перед + // входом в off-mode. Флаг ALRAF в RTC (домен резервного + // питания) и программная защёлка alarm_fired_latch + // переживают сброс/пропадание питания и не сбрасываются + // нигде, кроме off-mode. Будильник, разбудивший плату в + // прошлый раз, оставляет их взведёнными; без сброса первый + // же опрос off-mode прочитает это как свежее срабатывание и + // разбудит плату немедленно (~220 мс) вместо запрошенного + // времени. Сбрасываем обе защёлки, чтобы разбудил только + // свежий будильник (или дедлайн/кнопка). + rtc_alarm_take_fired(); + rtc_clear_alarm_flag(); + console_print_w_prefix("Suspend mode: on (power-off, wake by alarm)\r\n"); + } else { + console_print_w_prefix("Suspend mode: on\r\n"); + } + } else if (wbec_ctx.suspend_mode) { + wbec_ctx.suspend_mode = false; + // Разоружаем «сон начался» при выходе из режима, чтобы на + // следующем цикле кормление watchdog не завершило suspend + // немедленно (флаг живёт в ЕС между циклами - тёплый сброс + // и пробуждение по будильнику не перезапускают ЕС). + wbec_ctx.suspend_started = false; + console_print_w_prefix("Suspend mode: off (request)\r\n"); + } + } + } + if (linux_powerctrl_req == LINUX_POWERCTRL_OFF) { // Если прилетел запрос из линукса на выключение // Это была выполнена команда `poweroff` или `rtcwake -m off` @@ -546,13 +627,176 @@ 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; + } + + // Suspend-to-off: питание SoC выключено самим PMIC (кроме + // DRAM), Linux заморожен в самообновляющейся памяти. Будим + // PMIC по будильнику, дедлайну или кнопке: перезапуск + // последовательности включения "нажимает" PWRON, PMIC + // восстанавливает записанную конфигурацию питания. + if (wbec_ctx.suspend_mode && wbec_ctx.suspend_off_mode && + wbec_ctx.suspend_started) { + bool alarm = rtc_alarm_take_fired(); + bool deadline = systick_get_time_since_timestamp( + wbec_ctx.suspend_entry_timestamp) > wbec_ctx.suspend_timeout_ms; + bool button = pwrkey_handle_short_press(); + + if (alarm || deadline || button) { + wbec_ctx.suspend_mode = false; + wbec_ctx.suspend_off_mode = false; + // Разоружаем «сон начался»: пробуждение по будильнику - это + // resume того же ядра, ЕС не перезапускается, поэтому без + // сброса флаг остаётся взведён и на следующем цикле первое + // же кормление watchdog завершит suspend немедленно. + wbec_ctx.suspend_started = false; + // Это resume того же ядра (DRAM в самообновлении): бип на + // входе в WORKING сорвёт ре-инициализацию DRAM и уронит плату + // в cold-boot. Подавляем сигнал включения именно для этого + // пробуждения (см. suspend_resume_no_beep в new_state()). + wbec_ctx.suspend_resume_no_beep = true; + if (alarm) { + wbec_ctx.poweron_reason = REASON_RTC_ALARM; + } else if (button) { + wbec_ctx.poweron_reason = REASON_POWER_KEY; + } else { + wbec_ctx.poweron_reason = REASON_WATCHDOG; + } + console_print("\r\n\n"); + console_print_w_prefix("Suspend-to-off: wake up, restart PMIC via PWRON\r\n"); + linux_cpu_pwr_seq_wakeup(); + new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); + break; + } + // Кормления watchdog в этом режиме невозможны (Linux + // выключен) - потребляем возможный таймаут вхолостую + (void)wdt_handle_timed_out(); + break; + } + +#if defined(WBEC_HAS_WARM_RESET) + // Если Linux сбрасывает watchdog - система жива, + // разрешаем следующую попытку тёплого сброса. + // Это же событие завершает режим suspend: после пробуждения + // Linux первым делом снова начинает кормить watchdog. + if (wdt_handle_fed()) { + wbec_ctx.wd_warm_reset_attempted = false; + // Кормление завершает режим suspend только после того, как + // сон реально начался (3.3В пропадало): до заморозки системы + // демон watchdog продолжает кормить. + if (wbec_ctx.suspend_mode && wbec_ctx.suspend_started) { + wbec_ctx.suspend_mode = false; + wbec_ctx.suspend_started = false; + console_print_w_prefix("Suspend mode: off (watchdog fed)\r\n"); + } + } + + if (wbec_ctx.suspend_mode) { + // Во время объявленного сна watchdog-таймауты игнорируются + // (Linux заморожен и не кормит намеренно), но запрошенная + // длительность сна + запас служит дедлайном: если система + // не проснулась - обычное восстановление тёплым сбросом. + (void)wdt_handle_timed_out(); + if (systick_get_time_since_timestamp(wbec_ctx.suspend_entry_timestamp) > + wbec_ctx.suspend_timeout_ms) { + wbec_ctx.suspend_mode = false; + if (!wbec_ctx.suspend_started) { + // Сон так и не начался (suspend отменён или не + // дошёл до отключения питания) - система живёт, + // просто тихо выходим из режима + console_print_w_prefix("Suspend mode: off (never started)\r\n"); + break; + } + wbec_ctx.suspend_started = false; + wbec_ctx.wd_warm_reset_attempted = true; + wbec_ctx.poweron_reason = REASON_WATCHDOG_WARM; + console_print("\r\n\n"); + console_print_w_prefix("Suspend deadline passed, system did not wake up - warm reset.\r\n"); + linux_cpu_pwr_seq_warm_reset(); + new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); + break; + } + } else + // Если сработал WDT - сначала пробуем тёплый сброс (DRAM сохраняется, + // логи ramoops можно будет прочитать после перезагрузки). + // Если после тёплого сброса система не ожила (Linux не сбросил + // 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 + // Завершение режима suspend по первому кормлению watchdog - + // только после реального начала сна (3.3В пропадало) + if (wdt_handle_fed() && wbec_ctx.suspend_mode && wbec_ctx.suspend_started) { + wbec_ctx.suspend_mode = false; + wbec_ctx.suspend_started = false; + console_print_w_prefix("Suspend mode: off (watchdog fed)\r\n"); } + if (wbec_ctx.suspend_mode) { + // Во время объявленного сна watchdog-таймауты игнорируются; + // дедлайн - запрошенная длительность сна + запас. + (void)wdt_handle_timed_out(); + if (systick_get_time_since_timestamp(wbec_ctx.suspend_entry_timestamp) > + wbec_ctx.suspend_timeout_ms) { + wbec_ctx.suspend_mode = false; + if (!wbec_ctx.suspend_started) { + console_print_w_prefix("Suspend mode: off (never started)\r\n"); + break; + } + wbec_ctx.suspend_started = false; + wbec_ctx.poweron_reason = REASON_WATCHDOG; + console_print("\r\n\n"); + console_print_w_prefix("Suspend deadline passed, system did not wake up - reset power.\r\n"); + linux_cpu_pwr_seq_hard_reset(); + new_state(WBEC_STATE_POWER_ON_SEQUENCE_WAIT); + break; + } + } else // Если сработал WDT - перезагружаемся по питанию if (wdt_handle_timed_out()) { wbec_ctx.poweron_reason = REASON_WATCHDOG; @@ -561,13 +805,27 @@ 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 минут // Если питание пропадает слишком часто - выключаемся // Это происходит, например, при питании через плохой USB кабель. // В результате PMIC выключается, но питание на линии 5В остаётся. // Ограничение по числу попыток нужно, чтобы избежать циклического перезапуска. - if (!vmon_get_ch_status(VMON_CHANNEL_V33)) { + // В режиме suspend потеря 3.3В ожидаема: BL31 отключает DCDC1 + // на время сна - проверку пропускаем, но запоминаем факт + // пропадания: с этого момента сон действительно начался. + if (wbec_ctx.suspend_mode && !vmon_get_ch_status(VMON_CHANNEL_V33)) { + wbec_ctx.suspend_started = true; + } + if (!wbec_ctx.suspend_mode && !vmon_get_ch_status(VMON_CHANNEL_V33)) { console_print_w_prefix("3.3V is lost\r\n"); if (!vmon_get_ch_status(VMON_CHANNEL_V50)) { @@ -626,4 +884,11 @@ void wbec_do_periodic_work(void) { memset(&wbec_ctx, 0, sizeof(wbec_ctx)); } + + // «Сон начался» / разрешение выхода из suspend по кормлению watchdog. + // Должен возвращаться в исходное (false) состояние после выхода из режима. + bool utest_wbec_get_suspend_started(void) + { + return wbec_ctx.suspend_started; + } #endif diff --git a/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..8f37805 --- /dev/null +++ b/unittests/wbec-integration/wbec_integration_test.c @@ -0,0 +1,728 @@ +#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; +static bool alarm_fired; + +bool rtc_alarm_is_alarm_enabled(void) { return alarm_enabled; } + +bool rtc_alarm_take_fired(void) +{ + bool ret = alarm_fired; + alarm_fired = false; + return ret; +} + +bool temperature_control_is_temperature_ready(void) { return true; } +int16_t temperature_control_get_temperature_c_x100(void) { return 2500; } + +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); +bool utest_wbec_get_suspend_started(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 + +// Объявление окна suspend-to-off из Linux через регион SUSPEND_CTRL. +static void sim_announce_off_mode(uint16_t timeout_s) +{ + struct REGMAP_SUSPEND_CTRL s = { .timeout_s = timeout_s, .off_mode = 1 }; + regmap_set_region_data(REGMAP_REGION_SUSPEND_CTRL, &s, sizeof(s)); + utest_regmap_mark_region_changed(REGMAP_REGION_SUSPEND_CTRL); +} + +// Регрессия (rtcwake -m mem): устаревшая защёлка будильника, оставшаяся от +// прошлого пробуждения (ALRAF в домене резервного питания + программный +// alarm_fired_latch), НЕ должна будить плату сразу при входе в off-mode. +// До фикста первый же опрос off-mode читал это как свежее срабатывание и +// будил плату через ~220 мс вместо запрошенного времени сна. +static void test_suspend_off_stale_alarm_does_not_wake_immediately(void) +{ + sim.check_invariants = false; // off-mode: плата намеренно "не стартует" + sim.soc_feeds = true; + sim_boot_to_working(); + + uint32_t boots_before = sim.soc_boot_count; + + // Устаревшее событие будильника от прошлого пробуждения + alarm_fired = true; + + // Linux объявляет окно suspend-to-off (заведомо длинный дедлайн) + sim_announce_off_mode(3600); + sim_tick(); // вход в off-mode должен дренажировать защёлку + + TEST_ASSERT_FALSE_MESSAGE(alarm_fired, + "Off-mode entry must drain the stale software alarm-fired latch"); + TEST_ASSERT_TRUE_MESSAGE(utest_rtc_was_alarm_flag_cleared(), + "Off-mode entry must clear the hardware ALRAF flag"); + + // BL31 гасит 3.3В (в модели - как зависший PMIC: 3.3В пропало и не + // вернётся, пока EC не перезапустит PMIC пробуждением) + sim.pmic_crashed = true; + + // 5 секунд сна - без свежего будильника плата НЕ должна проснуться + sim_run_ms(5000); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_before, sim.soc_boot_count, + "Stale alarm latch must NOT wake the board immediately"); +} + +// Регрессия: свежий будильник, сработавший уже во время off-mode, обязан +// разбудить плату (тем же путём linux_cpu_pwr_seq_wakeup) - фикс не должен +// подавлять реальное пробуждение по будильнику. +static void test_suspend_off_fresh_alarm_wakes(void) +{ + sim.check_invariants = false; + sim.soc_feeds = true; + sim_boot_to_working(); + + uint32_t boots_before = sim.soc_boot_count; + + sim_announce_off_mode(3600); + sim_tick(); + sim.pmic_crashed = true; // BL31 снял 3.3В + sim_run_ms(2000); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_before, sim.soc_boot_count, + "Board must be asleep before a fresh alarm fires"); + + // Свежий будильник; PMIC при пробуждении восстанавливает 3.3В + alarm_fired = true; + sim.pmic_crashed = false; + sim_run_ms(3000); // время на последовательность пробуждения + + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_before, + "A fresh off-mode alarm must wake the board"); +} + +// Один цикл off-mode: объявление, пропадание 3.3В (сон начался), +// пробуждение по свежему будильнику (resume того же ядра), возврат в WORKING. +static void sim_off_mode_cycle(void) +{ + sim_announce_off_mode(60); + sim_tick(); + sim.pmic_crashed = true; // BL31 снял 3.3В -> arm взводится + sim_run_ms(500); + alarm_fired = true; // будильник будит + sim.pmic_crashed = false; + sim_run_ms(3000); // последовательность пробуждения -> WORKING +} + +// Регрессия (fail-without-fix): после выхода из off-mode по будильнику (resume +// того же ядра - ЕС не перезапускается) флаг «сон начался» / разрешение выхода +// из suspend по кормлению watchdog ОБЯЗАН вернуться в исходное (disarmed) +// состояние. Иначе на следующем цикле первое же кормление watchdog завершит +// suspend немедленно (на стенде: RF2+ [EC] Suspend mode: off (request)). +static void test_suspend_off_exit_disarms_feed_exit(void) +{ + sim.check_invariants = false; + sim.soc_feeds = true; + sim_boot_to_working(); + + sim_announce_off_mode(60); + sim_tick(); + sim.pmic_crashed = true; // 3.3В пропало -> arm взводится + sim_run_ms(500); + TEST_ASSERT_TRUE_MESSAGE(utest_wbec_get_suspend_started(), + "feed-exit arm must be set once the EC has seen the 3.3V drop"); + + alarm_fired = true; // будильник будит, resume того же ядра + sim.pmic_crashed = false; + sim_run_ms(3000); + + TEST_ASSERT_FALSE_MESSAGE(utest_wbec_get_suspend_started(), + "suspend-mode exit must disarm the feed-exit / seen-3.3V-drop flag"); +} + +// Регрессия многоцикловая: цикл 1 взводит arm и выходит по будильнику; на +// цикле 2 (watchdog работает и кормит во время входа) плата обязана так же +// уснуть и проснуться по будильнику, а не выйти из suspend по кормлению. +static void test_suspend_off_second_cycle_sleeps_with_watchdog(void) +{ + sim.check_invariants = false; + sim.soc_feeds = true; + sim_boot_to_working(); + + sim_off_mode_cycle(); // цикл 1 + uint32_t boots_after_c1 = sim.soc_boot_count; + + // Цикл 2: объявляем и кормим watchdog во время входа (до и после 3.3В) + sim_announce_off_mode(60); + sim_tick(); + soc_feed_watchdog(); + sim_run_ms(150); + sim.pmic_crashed = true; // 3.3В пропало + sim_run_ms(300); + soc_feed_watchdog(); // кормление при взведённом arm + sim_run_ms(1000); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_after_c1, sim.soc_boot_count, + "cycle 2 must stay asleep, not exit suspend on a watchdog feed"); + + alarm_fired = true; // свежий будильник + sim.pmic_crashed = false; + sim_run_ms(3000); + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_after_c1, + "cycle 2 must still wake on a fresh alarm"); +} + +int main(void) +{ + UNITY_BEGIN(); + + 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); + RUN_TEST(test_suspend_off_stale_alarm_does_not_wake_immediately); + RUN_TEST(test_suspend_off_fresh_alarm_wakes); + RUN_TEST(test_suspend_off_exit_disarms_feed_exit); + RUN_TEST(test_suspend_off_second_cycle_sleeps_with_watchdog); +#if defined(WBEC_HAS_WARM_RESET) + RUN_TEST(test_escalation_alternates_warm_and_hard); +#endif + + 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..889d23b 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; @@ -147,6 +163,15 @@ void utest_rtc_alarm_set_enabled(bool enabled) alarm_enabled = enabled; } +bool rtc_alarm_take_fired(void) +{ + return false; +} + +void linux_cpu_pwr_seq_wakeup(void) +{ +} + bool rtc_alarm_is_alarm_enabled(void) { return alarm_enabled; 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);