diff --git a/include/config.h b/include/config.h index 56ff81a..e66e22a 100644 --- a/include/config.h +++ b/include/config.h @@ -65,6 +65,15 @@ #define WBEC_PERIODIC_WAKEUP_FIRST_TIMEOUT_S 5 #define WBEC_PERIODIC_WAKEUP_NEXT_TIMEOUT_S 2 +// suspend-to-off: период периодического пробуждения (RTC WUT) во время окна +// сна в режиме STM32 Stop 1. Выбран < 10 с (период аппаратного IWDG): каждое +// пробуждение по WUT кормит IWDG и продвигает дедлайн-бэкстоп (systick в Stop +// не идёт). 4 с даёт запас к IWDG и пренебрежимо мало по току. +#define WBEC_SUSPEND_STOP_FEED_PERIOD_S 4 +// Сколько EC остаётся бодрствовать после пробуждения по фронту кнопки, давая +// антидребезгу (500 мс) подтвердить короткое нажатие, прежде чем снова уснуть. +#define WBEC_SUSPEND_STOP_BUTTON_GRACE_MS 700 + // Число попыток перезапуска PMIC при пропадании 3.3В // Если за указанное время 3.3В пропадёт больше, чем указанное число раз, // то EC выключит питание и уйдёт в спящий режим @@ -74,6 +83,11 @@ // Пищалка #define EC_BUZZER_BEEP_FREQ 1000 #define EC_BUZZER_BEEP_POWERON_MS 100 +// suspend-to-off: бип-подтверждение пробуждения по кнопке. Звучит в такте +// классификации (SoC ещё в сбросе, PMIC спит) и обязан закончиться до нажатия +// PWRON (~105 мс после классификации) - задолго до хрупкой ре-инициализации +// DRAM в SPL (~0.6 с), которую бип срывает (провал f3face8). +#define EC_BUZZER_BEEP_SUSPEND_WAKE_MS 100 #define EC_BUZZER_BEEP_SHORT_PRESS_MS 300 #define EC_BUZZER_BEEP_LONG_PRESS_MS 1000 diff --git a/include/gpio-subsystem.h b/include/gpio-subsystem.h index fcd2205..4776755 100644 --- a/include/gpio-subsystem.h +++ b/include/gpio-subsystem.h @@ -1,5 +1,12 @@ #pragma once +#include + void gpio_init(void); void gpio_reset(void); void gpio_do_periodic_work(void); + +// suspend-to-off: при on = true выключает V_OUT и держит его выключенным на всё +// окно сна (защита по V_IN в Stop не работает); при on = false возвращает +// штатное управление V_OUT. +void gpio_suspend(bool on); diff --git a/include/linux-power-control.h b/include/linux-power-control.h index 253083f..91d11ed 100644 --- a/include/linux-power-control.h +++ b/include/linux-power-control.h @@ -6,6 +6,13 @@ 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); +// suspend-to-off: запросить бип-подтверждение кнопочного пробуждения. Пищалка +// (SG1) запитана от 3.3В, которое во время окна сна ВЫКЛЮЧЕНО - звук физически +// невозможен до рестарта PMIC. Бип выдаётся последовательностью пробуждения в +// первый момент, когда 3.3В вернулось, а SoC ещё удержан в сбросе (безопасно +// для DRAM: ре-инициализация ещё не началась); импульс PWROK откладывается до +// конца бипа. +void linux_cpu_pwr_seq_wake_beep_request(void); void linux_cpu_pwr_seq_hard_off(void); void linux_cpu_pwr_seq_hard_reset(void); void linux_cpu_pwr_seq_reset_pmic(void); diff --git a/include/mcu-pwr.h b/include/mcu-pwr.h index 3f7ea66..5394fdf 100644 --- a/include/mcu-pwr.h +++ b/include/mcu-pwr.h @@ -1,4 +1,5 @@ #pragma once +#include #include "wbmcu_system.h" #include "config.h" @@ -21,3 +22,32 @@ void mcu_goto_standby(uint16_t wakeup_after_s); enum mcu_vcc_5v_state mcu_get_vcc_5v_last_state(void); void mcu_save_vcc_5v_last_state(enum mcu_vcc_5v_state state); void mcu_check_vbat_do_periodic_work(void); + +// --- suspend-to-off: окно сна в режиме STM32 Stop 1 --- +// В отличие от Standby, Stop сохраняет SRAM и защёлки GPIO и просыпается на +// месте (WFI возвращается, сброса НЕТ). Источники пробуждения заводятся через +// EXTI/NVIC, а не WKUP. Подробности — в ec-stop-mode-brief.md. + +// Однократно вооружает источники пробуждения окна Stop: +// - RTC (Alarm A + WUT, прямая линия EXTI 19) -> IMR19 + NVIC(RTC_TAMP_IRQn); +// - кнопка PWRON (PA0, фронт вниз = нажатие) -> EXTI0 + NVIC(EXTI0_1_IRQn); +// и маскирует EXTI9 (SoC-CS/SPI2), чтобы тёмный SoC не будил EC ложно. +// Идемпотентно; вызывать один раз при входе в окно. +void mcu_stop_window_prepare(void); +// Возвращает прерывания к состоянию до окна сна: снимает EXTI-источники +// пробуждения Stop (кнопка EXTI0, RTC EXTI19) и восстанавливает маску EXTI9, +// наложенную mcu_stop_window_prepare(). Вызывать один раз на выходе окна по +// реальному пробуждению (не на кормящих ре-входах). +void mcu_stop_window_finish(void); +// Кормит IWDG и уходит в STM32 Stop 1 (LPMS=001, SLEEPDEEP, WFI). Перед сном +// сбрасывает только устаревшие WUT/кнопка/CWUF (НИКОГДА не CALRAF — реальный +// будильник обязан разбудить). Блокируется до пробуждения по EXTI (тик WUT, +// будильник или фронт кнопки); на выходе снова кормит IWDG и восстанавливает +// тактирование 64 МГц, systick и АЦП. Возвращается после пробуждения. +void mcu_stop_enter(void); +// Потребляет признак «проснулись по тику WUT» (взводит ISR RTC). true один раз +// на каждый истёкший период WUT — база кормления IWDG и дедлайна. +bool mcu_stop_take_wut_tick(void); +// Потребляет признак «проснулись по фронту кнопки» (взводит ISR EXTI0). true +// один раз, если пробуждение было фронтом PWRON, ещё не прошедшим антидребезг. +bool mcu_stop_take_button_wake(void); diff --git a/include/rtc.h b/include/rtc.h index af8a1d0..038b586 100644 --- a/include/rtc.h +++ b/include/rtc.h @@ -35,6 +35,10 @@ void rtc_set_alarm(const struct rtc_alarm * alarm); uint16_t rtc_get_offset(void); void rtc_set_offset(uint16_t offeset); void rtc_clear_alarm_flag(void); +// Снимает ALRAIE (разрешение прерывания будильника) под разблокировкой WPR, НЕ +// трогая флаг ALRAF. Снимает прямую линию EXTI19 будильника в режиме Stop, +// сохраняя защёлку ALRAF для опросчика rtc_alarm_do_periodic_work. +void rtc_mask_alarm_irq(void); void rtc_enable_pc13_1hz_clkout(void); void rtc_disable_pc13_1hz_clkout(void); void rtc_enable_pa4_1hz_clkout(void); diff --git a/include/temperature-control.h b/include/temperature-control.h index 0547300..0e1796a 100644 --- a/include/temperature-control.h +++ b/include/temperature-control.h @@ -14,3 +14,8 @@ int16_t temperature_control_get_temperature_c_x100(void); // При force_enable = false нагреватель управляется по температуре void temperature_control_heater_force_control(bool force_enable); +// suspend-to-off: при on = true выключает нагреватель и держит его выключенным +// на всё окно сна (термостат и NTC в Stop не работают); при on = false +// возвращает управление по температуре. +void temperature_control_suspend(bool on); + diff --git a/include/voltage-monitor.h b/include/voltage-monitor.h index f3ccf04..74feb76 100644 --- a/include/voltage-monitor.h +++ b/include/voltage-monitor.h @@ -16,3 +16,10 @@ bool vmon_ready(void); bool vmon_get_ch_status(enum vmon_channel ch); bool vmon_check_ch_once(enum vmon_channel ch); void vmon_do_periodic_work(void); + +// Перевзводит окно «устаканивания» после пробуждения из STM32 Stop: делает +// vmon снова «не готов» и перезапускает 100 мс задержку. АЦП в Stop стоит, и +// первый DMA-отсчёт после пробуждения переинициализирует фильтр одним сырым +// измерением; без перевзвода одиночный низкий V50 мог бы увести +// последовательность питания в Standby. Фильтр АЦП при этом НЕ трогаем. +void vmon_suspend_rearm_settle(void); diff --git a/src/gpio-subsytem.c b/src/gpio-subsytem.c index b5c2c45..79739d8 100644 --- a/src/gpio-subsytem.c +++ b/src/gpio-subsytem.c @@ -63,6 +63,10 @@ struct gpio_ctx { uint16_t gpio_ctrl; uint16_t gpio_dir; uint16_t gpio_af; + // suspend-to-off: во время окна сна V_OUT держим выключенным. V_IN в Stop + // не измеряется, защита по перенапряжению (> 28 В) работать не может, а + // защёлка GPIO сохраняется — безопасное состояние — OFF. + bool suspend_off; }; static struct gpio_ctx gpio_ctx = { @@ -161,11 +165,25 @@ static void set_mod_gpio_af(void) static void control_v_out(void) { + if (gpio_ctx.suspend_off) { + // Окно suspend-to-off: держим V_OUT выключенным (защита по V_IN не + // может работать в Stop), не трогая пользовательский бит gpio_ctrl. + set_v_out_state(false); + return; + } bool v_in_is_in_proper_range = vmon_get_ch_status(VMON_CHANNEL_V_OUT); bool v_out_ctrl = gpio_ctx.gpio_ctrl & BIT(EC_EXT_GPIO_V_OUT); set_v_out_state(v_in_is_in_proper_range && v_out_ctrl); } +void gpio_suspend(bool on) +{ + gpio_ctx.suspend_off = on; + if (on) { + set_v_out_state(false); + } +} + static void set_mod_gpio_values(uint16_t new_ctrl) { gpio_ctx.gpio_ctrl_req_val = new_ctrl; diff --git a/src/linux-power-control.c b/src/linux-power-control.c index 6678f6c..559653f 100644 --- a/src/linux-power-control.c +++ b/src/linux-power-control.c @@ -12,6 +12,7 @@ #include "regmap-int.h" #include "wbmz-common.h" #include "wdt-stm32.h" +#include "buzzer.h" static const gpio_pin_t gpio_linux_power = { EC_GPIO_LINUX_POWER }; static const gpio_pin_t gpio_pmic_pwron = { EC_GPIO_LINUX_PMIC_PWRON }; @@ -27,6 +28,7 @@ enum pwr_state { PS_ON_STEP1_WAIT_3V3, // Ждём, пока появится 3.3В PS_ON_STEP2_PMIC_PWRON, // Если 3.3В не появляется, пробуем включить PMIC "нажатием" на PWRON PS_ON_STEP3_PMIC_PWRON_OFF_WAIT, // Если и это не помогло, отпускаем PWRON и делаем несколько попыток + PS_WAKE_BEEP_WAIT, // Бип-подтверждение кнопочного пробуждения (3.3В уже есть, SoC ещё в сбросе) PS_ON_COMPLETE, PS_RESET_5V_WAIT, // Нужно при перезаргрузке - выключаем 5В и ждём разрядку линий @@ -39,6 +41,8 @@ struct pwr_ctx { // импульс на PWROK - PMIC при выходе из сна восстанавливает // питание, но не выдаёт сброс, и SoC сам не стартует bool wake_pending; + // Бип-подтверждение кнопочного пробуждения, отложенный до возврата 3.3В + bool wake_beep; enum pwr_state state; systime_t timestamp; unsigned attempt; @@ -126,6 +130,11 @@ void linux_cpu_pwr_seq_off_and_goto_standby(uint16_t wakeup_after_s) mcu_goto_standby(wakeup_after_s); } +void linux_cpu_pwr_seq_wake_beep_request(void) +{ + pwr_ctx.wake_beep = true; +} + /** * @brief Включает питание линукс штатным способом: * Включается 5В, затем контролируется появление 3.3В. @@ -293,15 +302,42 @@ void linux_cpu_pwr_seq_do_periodic_work(void) // Питание восстановлено после сна PMIC: SoC ещё в // сбросе, толкаем его импульсом на PWROK pwr_ctx.wake_pending = false; + if (pwr_ctx.wake_beep) { + // Кнопочное пробуждение: первый момент, когда пищалка + // (запитана от 3.3В!) может звучать. SoC ещё в сбросе, + // DRAM в автономном самообновлении - безопасно (f3face8). + pwr_ctx.wake_beep = false; + buzzer_beep(EC_BUZZER_BEEP_FREQ, EC_BUZZER_BEEP_SUSPEND_WAKE_MS); + new_state(PS_WAKE_BEEP_WAIT); + break; + } pmic_reset_gpio_on(); new_state(PS_WARM_RESET_PULSE); break; } // Если 3.3В появилось, то считаем что питание включено new_state(PS_ON_COMPLETE); + break; + } + if (pwr_ctx.wake_pending) { + // Пробуждение из suspend-to-off: 5В не пропадало, PMIC спит и сам + // 3.3В никогда не поднимет - секундное ожидание самозапуска здесь + // гарантированно мёртвое время (~1 с на КАЖДОМ пробуждении, + // стенд 2026-07-08). Жмём PWRON сразу; показание V33 к этому + // моменту уже отфильтровано (вход в do_periodic_work закрыт + // vmon_ready() - 100 мс устаканивания после Stop), а ретраи + // STEP2/STEP3 остаются штатным fallback. Случай «3.3В присутствует» + // (поздний отказ SoC от сна) обработан веткой выше: импульс PWROK + // без нажатия. + console_print_w_prefix("Suspend-to-off: PMIC is sleeping, press PWRON now\r\n"); + pmic_pwron_gpio_on(); + pwr_ctx.attempt = 0; + new_state(PS_ON_STEP2_PMIC_PWRON); + break; } if (in_state_time_ms() > 1000) { - // Если 3.3В не появилось, то попробуем включить PMIC через PWRON + // Обычное включение (после подачи 5В PMIC должен стартовать сам). + // Если 3.3В не появилось за 1 с - пробуем включить PMIC через PWRON console_print_w_prefix("No voltage on 3.3V line, try to switch on PMIC throught PWRON\r\n"); pmic_pwron_gpio_on(); pwr_ctx.attempt = 0; @@ -318,6 +354,14 @@ void linux_cpu_pwr_seq_do_periodic_work(void) pmic_pwron_gpio_off(); if (pwr_ctx.wake_pending) { pwr_ctx.wake_pending = false; + if (pwr_ctx.wake_beep) { + // См. PS_ON_STEP1_WAIT_3V3: бип при вернувшемся 3.3В, + // SoC ещё в сбросе; PWROK - после бипа. + pwr_ctx.wake_beep = false; + buzzer_beep(EC_BUZZER_BEEP_FREQ, EC_BUZZER_BEEP_SUSPEND_WAKE_MS); + new_state(PS_WAKE_BEEP_WAIT); + break; + } pmic_reset_gpio_on(); new_state(PS_WARM_RESET_PULSE); break; @@ -349,6 +393,17 @@ void linux_cpu_pwr_seq_do_periodic_work(void) } break; + // Бип-подтверждение кнопочного пробуждения: 3.3В вернулось (пищалка + // запитана), SoC удержан в сбросе, DRAM в самообновлении - ре-инициализация + // ещё не начата, звук безопасен (урок f3face8). Импульс PWROK откладывается + // до конца бипа (+~120 мс к resume). + case PS_WAKE_BEEP_WAIT: + if (in_state_time_ms() > (EC_BUZZER_BEEP_SUSPEND_WAKE_MS + 20)) { + pmic_reset_gpio_on(); + new_state(PS_WARM_RESET_PULSE); + } + break; + // Сброс питания 5В case PS_RESET_5V_WAIT: if (in_state_time_ms() > WBEC_POWER_RESET_TIME_MS) { diff --git a/src/mcu-pwr.c b/src/mcu-pwr.c index 09a5317..23109b0 100644 --- a/src/mcu-pwr.c +++ b/src/mcu-pwr.c @@ -2,6 +2,11 @@ #include "config.h" #include "wbmcu_system.h" #include "rtc.h" +#include "rcc.h" +#include "systick.h" +#include "adc.h" +#include "voltage-monitor.h" +#include "wdt-stm32.h" static enum mcu_poweron_reason mcu_poweron_reason = MCU_POWERON_REASON_UNKNOWN; @@ -61,6 +66,152 @@ void mcu_goto_standby(uint16_t wakeup_after_s) }; } +// --- suspend-to-off: окно сна в режиме STM32 Stop 1 --- + +// Причины пробуждения из Stop, взводимые ISR и потребляемые главным циклом. +// В Stop, в отличие от Standby, ISR РЕАЛЬНО выполняется при выходе из WFI, +// поэтому обработчики должны снимать линию EXTI, НЕ разрушая защёлку ALRAF, +// которую опрашивает rtc_alarm_do_periodic_work. +static volatile bool stop_wut_tick_pending; +static volatile bool stop_button_wake_pending; + +// EXTI-линия 19 (RTC) — ПРЯМАЯ: в RPR1/FPR1 нет бита для неё, линия снимается +// только действием на флаги/IE самого RTC. +static void mcu_stop_rtc_irq_handler(void) +{ + if (RTC->SR & RTC_SR_WUTF) { + // Периодический тик: чистим WUTF (снимает линию), автоперезагрузка WUT + // сама выстрелит в следующем периоде. Классификатор WUTF не читает. + RTC->SCR = RTC_SCR_CWUTF; + stop_wut_tick_pending = true; + } + if (RTC->SR & RTC_SR_ALRAF) { + // Будильник: снимаем ALRAIE (снимает прямую линию EXTI19, линия = + // ALRAF & ALRAIE), но ALRAF НЕ чистим — его защёлку читает опросчик + // rtc_alarm_do_periodic_work в главном цикле. Будильник одноразовый и + // завершает suspend, поэтому оставить ALRAIE замаскированным до конца + // окна корректно. + // + // ВАЖНО: RTC_CR защищён WPR, а перед входом в Stop WPR ЗАБЛОКИРОВАН + // (rtc_set_periodic_wakeup() при вооружении окна завершается + // end_init_enable_wpr()). Голая запись `RTC->CR &= ~ALRAIE` здесь была + // бы молча проигнорирована железом: ALRAIE осталась бы взведённой, линия + // EXTI19 — асертнутой, и NVIC до бесконечности tail-chain'ил бы + // RTC_TAMP_IRQ, так что запрошенное Linux пробуждение по будильнику + // никогда бы не дошло до главного цикла (а IWDG досчитал бы до сброса). + // Поэтому снимаем ALRAIE через WPR-разблокированный помощник rtc.c. + rtc_mask_alarm_irq(); + } +} + +// EXTI-линия 0 (кнопка PA0) — конфигурируемая: FPR1 чистится напрямую. +static void mcu_stop_exti0_1_irq_handler(void) +{ + if (EXTI->FPR1 & EXTI_FPR1_FPIF0) { + EXTI->FPR1 = EXTI_FPR1_FPIF0; + stop_button_wake_pending = true; + } +} + +void mcu_stop_window_prepare(void) +{ + stop_wut_tick_pending = false; + stop_button_wake_pending = false; + + // RTC (Alarm A + WUT) — линия EXTI 19. Биты IE (WUTIE/ALRAIE) уже ставят + // rtc_set_periodic_wakeup()/rtc_set_alarm(); не хватает только размаскировки + // EXTI19 и NVIC — самая частая причина «RTC не будит из Stop» на G0. + NVIC_SetHandler(RTC_TAMP_IRQn, mcu_stop_rtc_irq_handler); + EXTI->IMR1 |= EXTI_IMR1_IM19; + NVIC_EnableIRQ(RTC_TAMP_IRQn); + + // Кнопка PWRON (PA0, активна низким): фронт вниз = нажатие. PA0 по сбросу + // уже замаплен на порт A в EXTICR, запись в EXTICR не нужна. + NVIC_SetHandler(EXTI0_1_IRQn, mcu_stop_exti0_1_irq_handler); + EXTI->FTSR1 |= EXTI_FTSR1_FT0; + EXTI->IMR1 |= EXTI_IMR1_IM0; + NVIC_EnableIRQ(EXTI0_1_IRQn); + + // Маскируем EXTI9 (SoC-CS/SPI2, оставлен размаскированным spi_slave_init): + // SoC во время сна тёмный, ложное пробуждение по CS не нужно. Снимаем маску + // только на выходе по реальному пробуждению (mcu_stop_window_finish). + EXTI->IMR1 &= ~EXTI_IMR1_IM9; + + // Глушим и сам SPI2 (NVIC): у обесточенного SoC линии SPI висят, и в + // бодрствующие окна (grace кнопки, кормящие тики) наводки на SCK/CS могут + // натактировать в regmap-движок МУСОРНЫЕ ЗАПИСИ - главный подозреваемый + // фантомного перепрограммирования будильника (стенд 2026-07-09 14:47: + // ALRAF за 570 с до срока). Первый CS-фронт после resume выполняет + // reset_and_init_spi() - связь с SoC самовосстанавливается. + NVIC_DisableIRQ(SPI2_IRQn); +} + +void mcu_stop_window_finish(void) +{ + // Возвращаем прерывания к состоянию до окна сна, чтобы штатная работа шла + // байт-в-байт как раньше: снимаем EXTI-источники пробуждения Stop + // (кнопка EXTI0 и RTC EXTI19) и восстанавливаем маску SoC-CS EXTI9. + EXTI->IMR1 &= ~(EXTI_IMR1_IM0 | EXTI_IMR1_IM19); + NVIC_DisableIRQ(EXTI0_1_IRQn); + NVIC_DisableIRQ(RTC_TAMP_IRQn); + EXTI->IMR1 |= EXTI_IMR1_IM9; + NVIC_EnableIRQ(SPI2_IRQn); + stop_wut_tick_pending = false; + stop_button_wake_pending = false; +} + +bool mcu_stop_take_wut_tick(void) +{ + bool ret = stop_wut_tick_pending; + stop_wut_tick_pending = false; + return ret; +} + +bool mcu_stop_take_button_wake(void) +{ + bool ret = stop_button_wake_pending; + stop_button_wake_pending = false; + return ret; +} + +void mcu_stop_enter(void) +{ + // E1: кормим IWDG прямо перед сном — максимальный запас до 10 с. + watchdog_reload(); + + // E2: сбрасываем ТОЛЬКО устаревшие не-будильниковые pending, чтобы не + // провалиться мгновенно. НИКОГДА не CALRAF: реальный будильник обязан + // разбудить (его защёлку читает опросчик, а не ISR). + RTC->SCR = RTC_SCR_CWUTF; + EXTI->FPR1 = EXTI_FPR1_FPIF0; + PWR->SCR = PWR_SCR_CWUF; + NVIC_ClearPendingIRQ(EXTI0_1_IRQn); + + // E3: Stop 1 (LPMS = 001) + PWR->CR1 = (PWR->CR1 & ~PWR_CR1_LPMS_Msk) | PWR_CR1_LPMS_0; + // E4: SLEEPDEEP + SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk; + // E5: барьер + сон + (void)PWR->CR1; + __DSB(); + __WFI(); + // ---- Пробуждение: cause-aware ISR (тик WUT / будильник / кнопка) уже отработал ---- + + // W1: сразу снова кормим IWDG. + watchdog_reload(); + // W2: возвращаем обычный сон для следующего WFI. + SCB->SCR &= ~SCB_SCR_SLEEPDEEP_Msk; + // W3: после Stop PLL выключен, SYSCLK = HSISYS 16 МГц — перезапираем 64 МГц. + rcc_set_hsi_pll_64mhz_clock(); + // W4: LOAD systick рассчитан на 64 МГц — пересчитываем. + systick_init(); + // W5: тактирование АЦП было остановлено — включаем и калибруем заново. + adc_init(ADC_CLOCK_DIV_64, ADC_VREF_INT); + // W6: перевзводим 100 мс окно «устаканивания» vmon, чтобы одиночный + // несглаженный отсчёт V50 не увёл последовательность пробуждения в Standby. + vmon_suspend_rearm_settle(); +} + enum mcu_vcc_5v_state mcu_get_vcc_5v_last_state(void) { if (rtc_get_tamper_reg(0) == MCU_VCC_5V_STATE_OFF) { diff --git a/src/rtc.c b/src/rtc.c index abe5456..74df22c 100644 --- a/src/rtc.c +++ b/src/rtc.c @@ -291,6 +291,25 @@ void rtc_clear_alarm_flag(void) RTC->SCR = RTC_SCR_CALRAF; } +void rtc_mask_alarm_irq(void) +{ + // Снимает разрешение прерывания будильника (ALRAIE), НЕ трогая флаг ALRAF. + // RTC_CR защищён от записи (WPR держится заблокированным в установившемся + // режиме), поэтому снимаем защиту на время записи — голая запись RTC->CR без + // разблокировки WPR аппаратно игнорируется. + // + // Используется ISR режима Stop: прямая линия EXTI19 будильника равна + // (ALRAF & ALRAIE); снятие ALRAIE снимает линию, но защёлку ALRAF сохраняет — + // её читает опросчик rtc_alarm_do_periodic_work в главном цикле, чтобы + // распознать пробуждение по будильнику. ALRAIE снова взводится при + // программировании следующего будильника (rtc_set_alarm). + // + // Смена ALRAIE не требует INIT-режима (в отличие от календаря/PRER/WUT). + disable_wpr(); + RTC->CR &= ~RTC_CR_ALRAIE; + enable_wpr(); +} + uint16_t rtc_get_offset(void) { return RTC->CALR; diff --git a/src/temperature-control.c b/src/temperature-control.c index bfebaf4..6b6c0a3 100644 --- a/src/temperature-control.c +++ b/src/temperature-control.c @@ -24,6 +24,11 @@ static inline fix16_t get_temperature(void) struct heater_ctx { bool enabled; bool force_enabled; + // suspend-to-off: во время окна сна нагреватель держим выключенным. + // NTC в Stop не измеряется, термостат не работает, а защёлка GPIO + // сохраняется — оставленный включённым нагреватель грелся бы без + // контроля и разрушал бы бюджет по питанию. + bool suspend_off; }; static struct heater_ctx heater_ctx; @@ -42,6 +47,14 @@ static inline fix16_t get_temperature(void) static void heater_control(void) { + if (heater_ctx.suspend_off) { + // Окно suspend-to-off: держим нагреватель выключенным. + if (heater_ctx.enabled) { + heater_disable(); + } + return; + } + if (heater_ctx.force_enabled) { // Нагреватель включили через regmap принудительно return; @@ -87,6 +100,18 @@ void temperature_control_do_periodic_work(void) #endif } +void temperature_control_suspend(bool on) +{ + #if defined EC_GPIO_HEATER + heater_ctx.suspend_off = on; + if (on) { + heater_disable(); + } + #else + (void)on; + #endif +} + void temperature_control_heater_force_control(bool force_enable) { #if defined EC_GPIO_HEATER diff --git a/src/usart_tx.c b/src/usart_tx.c index a88d3b0..27f167d 100644 --- a/src/usart_tx.c +++ b/src/usart_tx.c @@ -33,7 +33,17 @@ static inline void usart_transmit_char(char c) static inline void usart_wait_tranmission_complete(void) { - while (D_USART->ISR & USART_ISR_TC) {}; + // Ждём завершения передачи: TC = 1, когда последний кадр ПОЛНОСТЬЮ вышел + // из сдвигового регистра. Условие было инвертировано: во время передачи + // TC = 0, функция возвращалась мгновенно, и «блокирующая» печать + // завершалась с 1-2 кадрами ещё в полёте. Это стреляло в одном месте: + // печать маркера в такте вооружения окна suspend-to-off - через считанные + // микросекунды EC уходил в Stop, USART замерзал ПОСРЕДИ КАДРА, и линия TX + // висела на уровне бита данных всё окно. Отладочный мост питался мусором + // (хвосты пробелов за маркером в каждом окне), а на длинном окне зависал + // целиком - захват слепой (стенд 2026-07-09 00:37, «кнопка молчит»). + // Цена честного ожидания: <= 2 кадра ~ 170 мкс на печать. + while ((D_USART->ISR & USART_ISR_TC) == 0) {}; } static inline void init_debug_uart_if_not_initialized(void) diff --git a/src/voltage-monitor.c b/src/voltage-monitor.c index c11375a..081b205 100644 --- a/src/voltage-monitor.c +++ b/src/voltage-monitor.c @@ -48,6 +48,12 @@ bool vmon_ready(void) return vmon_initialized; } +void vmon_suspend_rearm_settle(void) +{ + vmon_initialized = 0; + start_timestamp = systick_get_system_time_ms(); +} + bool vmon_get_ch_status(enum vmon_channel ch) { return vmon_ch_status[ch]; diff --git a/src/wbec.c b/src/wbec.c index d000f8f..e1324d9 100644 --- a/src/wbec.c +++ b/src/wbec.c @@ -23,6 +23,7 @@ #include "hwrev.h" #include "buzzer.h" #include "temperature-control.h" +#include "gpio-subsystem.h" #include "wbmz-common.h" #include "wdt-stm32.h" @@ -99,6 +100,37 @@ struct wbec_ctx { // только питание DRAM). Пробуждение делает EC: по будильнику // (или дедлайну/кнопке) перезапускает PMIC импульсом PWRON. bool suspend_off_mode; + // suspend-to-off Stop 1: окно сна вооружено (EXTI-источники заведены, + // выходы переведены в безопасное состояние, WUT арм-нут один раз). + // Живёт в SRAM через Stop (сброса нет); разоружается на выходе. + bool stop_armed; + // Счётчик истёкших периодов WUT за окно (systick в Stop заморожен, поэтому + // дедлайн считаем по тикам WUT, а не по systick). Сбрасывается при арме. + uint32_t suspend_wut_ticks; + // Проснулись по фронту кнопки, короткое нажатие ещё не подтверждено + // антидребезгом: остаёмся бодрствовать (не уходим в Stop), пока pwrkey + // не разберёт нажатие или не истечёт окно ожидания. + bool stop_button_wake; + systime_t stop_button_wake_ts; + // Кнопка физически удерживалась в момент выхода из окна сна: её отпускание + // придёт уже после resume и НЕ должно породить новое «короткое нажатие» + // (в !linux_booted-ветке это немедленный hard off - стенд 2026-07-09). + // Глотаем события кнопки до наблюдаемого отпускания. + bool pwrkey_swallow; + systime_t pwrkey_swallow_ts; + // Пробуждение из окна suspend-to-off: Linux НЕ загружается заново, а + // ВОЗОБНОВЛЯЕТСЯ (DRAM жива). Завершение последовательности включения + // обязано сохранить linux_booted=true - иначе первые 20 с после resume + // короткое нажатие попадает в ветку «линукс не загружен» и жёстко рубит + // питание (стенд 2026-07-09 17:33). Холодные включения (standby-poweroff, + // подача питания, watchdog-сбросы) этот флаг не взводят. + bool suspend_wake_resume; + // Предыдущий антидребезженный уровень кнопки: IRQ_PWR_OFF_REQ взводится + // по ФРОНТУ нажатия, один раз на одно физическое нажатие. Раньше флаг + // взводился каждый проход, пока кнопка удержана: Linux успевал квитировать + // IRQ, и следующий проход взводил его снова - одно нажатие доезжало до + // драйвера дважды (стенд 2026-07-09 17:34, двойной "PWR_OFF_REQ latched"). + bool pwrkey_prev_level; // Взводится при пробуждении из suspend-to-off, чтобы подавить // звуковой сигнал включения на этом входе в WBEC_STATE_WORKING. // ПОЧЕМУ это критично: resume из suspend-to-off — это ре-инициализация @@ -480,9 +512,14 @@ void wbec_do_periodic_work(void) wdt_start_reset(); wdt_handle_timed_out(); // Флаг linux_initial_powered_on нужен чтобы понять, что линукс уже работал на момент включения ЕС - // В этом случае считаем его уже загруженным - wbec_ctx.linux_booted = wbec_ctx.linux_initial_powered_on; + // В этом случае считаем его уже загруженным. + // suspend_wake_resume: пробуждение из suspend-to-off - Linux + // ВОЗОБНОВЛЯЕТСЯ, а не загружается, поэтому остаётся «загруженным» + // (иначе нажатие в первые 20 с после resume жёстко рубит питание). + wbec_ctx.linux_booted = wbec_ctx.linux_initial_powered_on || + wbec_ctx.suspend_wake_resume; wbec_ctx.linux_initial_powered_on = false; + wbec_ctx.suspend_wake_resume = false; wbec_ctx.pwrkey_pressed = false; // Как только питание включилось - переходим в рабочий режим new_state(WBEC_STATE_WORKING); @@ -498,15 +535,39 @@ void wbec_do_periodic_work(void) wbec_ctx.linux_booted = true; } - if (wbec_ctx.linux_booted) { + if (wbec_ctx.pwrkey_swallow) { + // Хвост нажатия, пережившего окно сна: глотаем «короткое нажатие» + // до наблюдаемого отпускания. Нажатие могло быть ещё НЕ + // подтверждённым на выходе из окна (сырой уровень нажат, антидребезг + // не истёк) - тогда антидребезженный уровень читается «отпущено» ещё + // до PWRKEY_DEBOUNCE_MS после реального начала нажатия. Поэтому + // снимаем глотание только после того, как уровень непрерывно + // «отпущено» дольше антидребезга с запасом - к этому моменту сырое + // нажатие «в полёте» невозможно. Долгое нажатие (принудительное + // выключение) обрабатывается раньше по циклу в + // linux_cpu_pwr_seq_do_periodic_work и не глотается. + (void)pwrkey_handle_short_press(); + if (pwrkey_pressed()) { + wbec_ctx.pwrkey_swallow_ts = systick_get_system_time_ms(); + } else if (systick_get_time_since_timestamp(wbec_ctx.pwrkey_swallow_ts) > + (PWRKEY_DEBOUNCE_MS + 200)) { + wbec_ctx.pwrkey_swallow = false; + } + } else if (wbec_ctx.linux_booted) { // Если линукс загружен - отправляем запрос на выключение // При этом не ждём полноценного нажатия, а отправляем запрос сразу // Если выполняется долгое нажатие, то есть шанс что линукс успеет // корректно выключиться if (pwrkey_pressed()) { + if (!wbec_ctx.pwrkey_prev_level) { + // Только ФРОНТ нажатия: один IRQ на одно физическое + // нажатие. Взводить каждый проход нельзя: Linux квитирует + // IRQ, пока кнопка ещё удержана, и то же нажатие доезжает + // до драйвера вторым событием (стенд 2026-07-09 17:34). + irq_set_flag(IRQ_PWR_OFF_REQ); + } wbec_ctx.pwrkey_pressed = true; wbec_ctx.pwrkey_pressed_timestamp = systick_get_system_time_ms(); - irq_set_flag(IRQ_PWR_OFF_REQ); } } else { // Если линукс не загружен - выключаемся по питанию сразу же @@ -517,6 +578,9 @@ void wbec_do_periodic_work(void) new_state(WBEC_STATE_POWER_OFF_SEQUENCE_WAIT); } } + // Трек фронта кнопки для взведения IRQ (обновляется в любом состоянии + // ветки выше, включая «глотание», чтобы фронт не был ложным после него) + wbec_ctx.pwrkey_prev_level = pwrkey_pressed(); // Если флаг нажатой кнопки висит слишком долго (линукс по каким-то причинам не отреагировал) // Нужно его сбросить, т.к. он влияет на решение для выключения по poweroff @@ -658,47 +722,174 @@ void wbec_do_periodic_work(void) break; } - // Suspend-to-off: питание SoC выключено самим PMIC (кроме - // DRAM), Linux заморожен в самообновляющейся памяти. Будим - // PMIC по будильнику, дедлайну или кнопке: перезапуск - // последовательности включения "нажимает" PWRON, PMIC - // восстанавливает записанную конфигурацию питания. + // Suspend-to-off: питание SoC выключено самим PMIC (кроме DRAM), Linux + // заморожен в самообновляющейся памяти. Как только сон реально начался + // (3.3В пропало -> suspend_started), EC уходит в STM32 Stop 1 между + // пробуждениями, вместо того чтобы жечь ток на 64 МГц весь сон. Stop + // сохраняет SRAM и защёлки GPIO и просыпается на месте (сброса НЕТ), + // поэтому провала на линиях PMIC, погубившего Standby-подход, здесь нет + // по построению. Пробуждаемся по будильнику (запрос Linux), дедлайну + // (бэкстоп через тики RTC WUT) или кнопке (фронт EXTI). Классификация — + // в начале блока: потребляем защёлки, которые драйверы супер-цикла + // (pwrkey/rtc-alarm) успели выставить в этом же проходе. WFI — в самом + // низу; после пробуждения возвращаемся в супер-цикл. См. + // ec-stop-mode-brief.md. 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 (!wbec_ctx.stop_armed) { + // Управляемые супер-циклом выходы — в безопасное состояние на + // всё окно: их мониторинг (NTC, V_IN) в Stop заморожен, а + // защёлки GPIO сохраняются (см. brief §6). + temperature_control_suspend(true); // PD2: нагреватель off + gpio_suspend(true); // PD0: V_OUT off + // WUT арм-нут ОДИН раз: он кормит IWDG (< 10 с) и служит тиком + // дедлайна. Повторный арм каждый цикл входил бы в INIT-режим + // RTC и останавливал календарь — единственную базу времени Stop. + rtc_set_periodic_wakeup(WBEC_SUSPEND_STOP_FEED_PERIOD_S); + mcu_stop_window_prepare(); // EXTI/NVIC + маска SoC-CS + wbec_ctx.suspend_wut_ticks = 0; + wbec_ctx.stop_button_wake = false; + wbec_ctx.stop_armed = true; + // Диагностика: помечаем РЕАЛЬНОЕ начало окна сна. Между + // объявлением (0xA4) и пропаданием 3.3В SoC может входить в + // suspend десятки секунд; без этой строки по логу стенда не + // отличить «EC ещё не спал» от «EC проснулся мгновенно» + // (ложный диагноз 2026-07-08). + console_print("\r\n\n"); + console_print_w_prefix("Suspend-to-off: EC sleeping in Stop 1 (wake: alarm/button/deadline)\r\n"); + } - if (alarm || deadline || button) { + // Классификация пробуждения. + bool alarm = rtc_alarm_take_fired(); + // Кнопка: пробуждение по ПОДТВЕРЖДЕНИЮ нажатия - антидребезженный + // уровень «нажато» (+500 мс удержания) после фронта, разбудившего + // EC, - а НЕ по отпусканию. UX-контракт (Evgeny, 2026-07-09): + // «нажал - услышал бип (кнопка ещё в руке) - отпустил». Бип звучит + // на возврате 3.3В (~+0.3 с после подтверждения), пока держат. + // Резервно потребляем и защёлку короткого нажатия - отпускание, + // успевшее пройти целиком (защёлка не должна пережить окно). + bool button = (wbec_ctx.stop_button_wake && pwrkey_pressed()) || + pwrkey_handle_short_press(); + // Дедлайн — по накопленным тикам WUT, НЕ по systick (в Stop мёртв): + // suspend_timeout_ms уже с запасом +10 с (см. запись SUSPEND_CTRL). + bool deadline = ((uint64_t)wbec_ctx.suspend_wut_ticks * + WBEC_SUSPEND_STOP_FEED_PERIOD_S * 1000) >= + wbec_ctx.suspend_timeout_ms; + + if (alarm || button || deadline) { + // Нажатие «в полёте» на момент выхода: уровень уже нажат ИЛИ + // фронт видели, но антидребезг ещё не подтвердил (пробуждение + // по будильнику/дедлайну посреди начинающегося нажатия). Его + // подтверждение и отпускание придут ПОСЛЕ выхода и должны быть + // проглочены (см. pwrkey_swallow ниже). + bool press_in_flight = pwrkey_pressed() || wbec_ctx.stop_button_wake; wbec_ctx.suspend_mode = false; wbec_ctx.suspend_off_mode = false; - // Разоружаем «сон начался»: пробуждение по будильнику - это - // resume того же ядра, ЕС не перезапускается, поэтому без - // сброса флаг остаётся взведён и на следующем цикле первое - // же кормление watchdog завершит suspend немедленно. + // Разоружаем «сон начался»: пробуждение - это resume того же + // ядра, ЕС не перезапускается, поэтому без сброса флаг остался + // бы взведён и на следующем цикле первое же кормление watchdog + // завершило бы suspend немедленно. wbec_ctx.suspend_started = false; - // Это resume того же ядра (DRAM в самообновлении): бип на - // входе в WORKING сорвёт ре-инициализацию DRAM и уронит плату - // в cold-boot. Подавляем сигнал включения именно для этого - // пробуждения (см. suspend_resume_no_beep в new_state()). + wbec_ctx.stop_armed = false; + wbec_ctx.stop_button_wake = false; + // Это resume того же ядра (DRAM в самообновлении): бип на входе + // в WORKING сорвёт ре-инициализацию DRAM и уронит плату в + // cold-boot. Подавляем сигнал включения (см. new_state()). wbec_ctx.suspend_resume_no_beep = true; + // Linux возобновляется, а не загружается: сохранить + // linux_booted через завершение последовательности включения. + wbec_ctx.suspend_wake_resume = true; if (alarm) { wbec_ctx.poweron_reason = REASON_RTC_ALARM; } else if (button) { wbec_ctx.poweron_reason = REASON_POWER_KEY; + // Требование UX: нажатие в окне сна -> бип + пробуждение. + // Пищать ЗДЕСЬ бессмысленно: пищалка (SG1) запитана от + // 3.3В, которое во время окна ВЫКЛЮЧЕНО (стенд 2026-07-09: + // вызов buzzer_beep с валидными параметрами - и тишина, + // диагностическая сборка 93d307e). Бип + // откладывается в последовательность пробуждения: он + // прозвучит в первый момент возврата 3.3В, пока SoC ещё в + // сбросе (DRAM в самообновлении, ре-инициализация не + // начата - безопасно, урок f3face8), а PWROK - после бипа. + // Бип на входе в WORKING остаётся ПОДАВЛЕННЫМ + // (suspend_resume_no_beep). Будильник и дедлайн будят + // МОЛЧА - необслуживаемые пробуждения. + linux_cpu_pwr_seq_wake_beep_request(); } else { wbec_ctx.poweron_reason = REASON_WATCHDOG; } + // Нажатие кнопки в окне сна потреблено как ПРИЧИНА ПРОБУЖДЕНИЯ + // (или случилось, пока Linux был обесточен) - гасим его признаки + // для Linux. Блок «linux_booted -> irq_set_flag(IRQ_PWR_OFF_REQ)» + // выше по WORKING отработал и для этого нажатия (для EC Linux + // «включён» весь suspend), флаг никем не квитируется (Linux + // спит) и доехал бы до драйвера wbec-pwrkey после resume вторым + // событием: одно нажатие = пробуждение + poweroff от logind + + // cold boot (стенд 2026-07-08). Нажатие в обычной работе + // по-прежнему доставляется как раньше: этот сброс происходит + // только на выходе из окна suspend-to-off. То же скрытое + // поведение есть и на базовой a655128 (busy-poll) - см. коммит. + wbec_ctx.pwrkey_pressed = false; + irq_clear_flags(1u << IRQ_PWR_OFF_REQ); + // Если нажатие ещё «в полёте» (кнопка удерживается - теперь + // это ШТАТНЫЙ случай кнопочного пробуждения-по-подтверждению, - + // или фронт видели без подтверждения), его подтверждение/ + // отпускание придёт после resume: глотаем события кнопки до + // наблюдаемого отпускания (см. WORKING). + wbec_ctx.pwrkey_swallow = press_in_flight; + wbec_ctx.pwrkey_swallow_ts = systick_get_system_time_ms(); + // Возвращаем выходы под штатное управление и снимаем маску + // SoC-CS EXTI; резервный WUT-дедлайн больше не нужен. + temperature_control_suspend(false); + gpio_suspend(false); + mcu_stop_window_finish(); + rtc_disable_periodic_wakeup(); 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 - // выключен) - потребляем возможный таймаут вхолостую + + // Пробуждение было фронтом кнопки, но короткое нажатие ещё не прошло + // антидребезг (500 мс, нужен непрерывный systick). Остаёмся в + // бодрствующем супер-цикле, чтобы pwrkey_do_periodic_work разобрал + // нажатие штатно, и НЕ уходим в Stop, пока не истечёт окно ожидания. + // Так сохраняется фильтр случайных касаний (в отличие от «будим по + // самому фронту»), ценой ~1 с бодрствования на событие. + if (mcu_stop_take_button_wake()) { + wbec_ctx.stop_button_wake = true; + wbec_ctx.stop_button_wake_ts = systick_get_system_time_ms(); + } + if (wbec_ctx.stop_button_wake) { + if (pwrkey_pressed()) { + // Кнопка ещё УДЕРЖИВАЕТСЯ (антидребезженный уровень): + // продлеваем окно ожидания - короткое нажатие фиксируется + // только на ОТПУСКАНИИ. Иначе удержание дольше grace-окна + // (~1.2 с) роняло EC обратно в Stop посреди нажатия + // (стенд 2026-07-09 14:46: 4 подряд брошенных нажатия). + wbec_ctx.stop_button_wake_ts = systick_get_system_time_ms(); + break; + } + if (systick_get_time_since_timestamp(wbec_ctx.stop_button_wake_ts) < + (PWRKEY_DEBOUNCE_MS + WBEC_SUSPEND_STOP_BUTTON_GRACE_MS)) { + break; // остаёмся бодрствовать, ждём разбор нажатия + } + // Нажатие не подтвердилось за окно ожидания — снова спим. + wbec_ctx.stop_button_wake = false; + } + + // Реальной причины нет: кормим IWDG и уходим в Stop до следующего + // тика WUT / будильника / кнопки. Кормления soft-WDT (Linux + // выключен) потребляем вхолостую. (void)wdt_handle_timed_out(); + watchdog_reload(); // E1: кормим IWDG перед сном + mcu_stop_enter(); // E2-E5 + восстановление + if (mcu_stop_take_wut_tick()) { + wbec_ctx.suspend_wut_ticks++; // W7: тик дедлайна + } break; } diff --git a/unittests/linux-power-control/linux_power_control_test.c b/unittests/linux-power-control/linux_power_control_test.c index 5c4a942..957f053 100644 --- a/unittests/linux-power-control/linux_power_control_test.c +++ b/unittests/linux-power-control/linux_power_control_test.c @@ -15,6 +15,14 @@ void utest_linux_power_control_reset_state(void); +// Заглушка пищалки: реальный linux-power-control.c пищит бип-подтверждение +// кнопочного пробуждения (PS_WAKE_BEEP_WAIT); здесь звук не проверяется. +void buzzer_beep(uint16_t freq, uint16_t duration_ms) +{ + (void)freq; + (void)duration_ms; +} + static const gpio_pin_t linux_power_gpio = {EC_GPIO_LINUX_POWER}; static const gpio_pin_t pmic_pwron_gpio = {EC_GPIO_LINUX_PMIC_PWRON}; static const gpio_pin_t pmic_reset_gpio = {EC_GPIO_LINUX_PMIC_RESET_PWROK}; diff --git a/unittests/mcu-pwr/Makefile b/unittests/mcu-pwr/Makefile new file mode 100644 index 0000000..15dedcc --- /dev/null +++ b/unittests/mcu-pwr/Makefile @@ -0,0 +1,29 @@ +# This test name +TEST_NAME = mcu_pwr_test + +# Project root directory +PROJ_DIR = ../.. + +# Source files to be checked (REAL mcu-pwr.c against a register-stub env) +TESTED_SRC += $(PROJ_DIR)/src/mcu-pwr.c + +# Auxiliary source files used in tests +AUX_SRC += utest_mcu_reg_env.c + +# Include directories. +# ВАЖНО: локальный каталог (.) идёт ПЕРВЫМ, чтобы наш register-stub +# wbmcu_system.h перекрывал реальный device-header. system/include НЕ добавляем. +# Каталог заглушек wdt-stm32 даёт declaration-only wdt-stm32.h (без IWDG). +INC += . +INC += $(PROJ_DIR)/unittests/utest_helpers/wdt-stm32 +INC += $(PROJ_DIR)/include +INC += $(PROJ_DIR)/libfixmath/libfixmath + +# List of tests +TEST_LIST = mcu_pwr_test + +# Compiler defs +DEFS += UNITY_OUTPUT_COLOR +DEFS += MODEL_WB85 + +include $(PROJ_DIR)/system/build_unittests.mk diff --git a/unittests/mcu-pwr/mcu_pwr_test.c b/unittests/mcu-pwr/mcu_pwr_test.c new file mode 100644 index 0000000..ef7ef6e --- /dev/null +++ b/unittests/mcu-pwr/mcu_pwr_test.c @@ -0,0 +1,285 @@ +#include "unity.h" + +#include "mcu-pwr.h" +#include "config.h" +#include "utest_mcu_reg_env.h" + +/** + * Register-stub тест РЕАЛЬНОГО src/mcu-pwr.c. + * + * Интеграционный харнесс (unittests/wbec-integration) полностью МОКАЕТ mcu-pwr, + * поэтому реальные ISR режима Stop, записи LPMS/SLEEPDEEP и снятие линии + * будильника не исполнялись ни одним тестом — из-за чего blocker с застреванием + * пробуждения по будильнику прошёл CI «зелёным». Здесь mcu-pwr.c собирается + * поверх управляемых тестом регистров (RTC/EXTI/PWR/SCB), а NVIC/WFI + * перехватываются, чтобы проверить именно поведение на уровне регистров/ISR. + */ + +void setUp(void) +{ + utest_reg_env_reset(); +} + +void tearDown(void) +{ +} + +// ---- Вооружение окна: EXTI/NVIC источники пробуждения ---- +// mcu_stop_window_prepare должен размаскировать EXTI19 (RTC) и EXTI0 (кнопка), +// завести падающий фронт на PA0, замаскировать EXTI9 (SoC-CS) и включить оба +// NVIC-вектора с зарегистрированными обработчиками. +static void test_stop_prepare_arms_exti_and_nvic(void) +{ + // spi_slave_init оставил EXTI9 (SoC-CS) размаскированным до входа в окно. + EXTI->IMR1 = EXTI_IMR1_IM9; + + mcu_stop_window_prepare(); + + TEST_ASSERT_TRUE_MESSAGE(EXTI->IMR1 & EXTI_IMR1_IM19, + "RTC line EXTI19 must be unmasked"); + TEST_ASSERT_TRUE_MESSAGE(EXTI->IMR1 & EXTI_IMR1_IM0, + "button line EXTI0 must be unmasked"); + TEST_ASSERT_TRUE_MESSAGE(EXTI->FTSR1 & EXTI_FTSR1_FT0, + "button PA0 must trigger on the falling edge (press)"); + TEST_ASSERT_FALSE_MESSAGE(EXTI->IMR1 & EXTI_IMR1_IM9, + "SoC-CS line EXTI9 must be masked for the sleep window"); + + TEST_ASSERT_TRUE_MESSAGE(utest_nvic_is_enabled(RTC_TAMP_IRQn), + "RTC_TAMP_IRQn must be NVIC-enabled"); + TEST_ASSERT_TRUE_MESSAGE(utest_nvic_is_enabled(EXTI0_1_IRQn), + "EXTI0_1_IRQn must be NVIC-enabled"); + TEST_ASSERT_NOT_NULL_MESSAGE(utest_nvic_get_handler(RTC_TAMP_IRQn), + "RTC ISR must be registered"); + TEST_ASSERT_NOT_NULL_MESSAGE(utest_nvic_get_handler(EXTI0_1_IRQn), + "button ISR must be registered"); + + // SPI2 (regmap-движок) обязан быть заглушен на окно: у обесточенного SoC + // линии SPI висят и наводки могут натактировать мусорные regmap-записи + // (фантомное перепрограммирование будильника, стенд 2026-07-09). + TEST_ASSERT_FALSE_MESSAGE(utest_nvic_is_enabled(SPI2_IRQn), + "SPI2 NVIC must be disabled for the sleep window"); + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(1, + utest_nvic_get_disable_count(SPI2_IRQn), + "prepare must actually issue NVIC_DisableIRQ for SPI2"); +} + +// ---- Вход в Stop 1: точные записи регистров сна ---- +// Требования брифа §7: LPMS = 001 (Stop 1, НЕ 011/Standby), SLEEPDEEP взведён на +// момент сна, чистятся ТОЛЬКО устаревшие CWUTF/FPR0/CWUF, но НИКОГДА CALRAF, +// EXTI0 pending снимается в NVIC, IWDG кормится до и после сна. +static void test_stop_enter_writes_stop1_registers(void) +{ + // Мусор в LPMS до входа — должен быть перезаписан ровно на 001. + PWR->CR1 = PWR_CR1_LPMS_0 | PWR_CR1_LPMS_1; // 011 (Standby) — «до» + + mcu_stop_enter(); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, utest_wfi_count(), + "mcu_stop_enter must execute exactly one WFI"); + + // LPMS = 001 (Stop 1), не 011 (Standby) — и на снимке WFI, и после. + TEST_ASSERT_EQUAL_UINT32_MESSAGE(PWR_CR1_LPMS_0, + utest_wfi_pwr_cr1() & PWR_CR1_LPMS_Msk, + "LPMS must be 001 (Stop 1) at the moment of sleep"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(PWR_CR1_LPMS_0, PWR->CR1 & PWR_CR1_LPMS_Msk, + "LPMS must remain 001 (Stop 1), never 011 (Standby)"); + + // SLEEPDEEP взведён на момент сна (в W-фазе снимается — проверяем на снимке). + TEST_ASSERT_TRUE_MESSAGE(utest_wfi_scb_scr() & SCB_SCR_SLEEPDEEP_Msk, + "SLEEPDEEP must be set at the WFI"); + TEST_ASSERT_FALSE_MESSAGE(SCB->SCR & SCB_SCR_SLEEPDEEP_Msk, + "SLEEPDEEP must be cleared again after wake"); + + // Чистятся только устаревшие не-будильниковые флаги; ALRAF (CALRAF) — НИКОГДА. + TEST_ASSERT_EQUAL_UINT32_MESSAGE(RTC_SCR_CWUTF, RTC->SCR, + "only CWUTF must be written to RTC->SCR (drain stale WUT)"); + TEST_ASSERT_FALSE_MESSAGE(RTC->SCR & RTC_SCR_CALRAF, + "CALRAF must NEVER be written on Stop entry (a real alarm must wake)"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(EXTI_FPR1_FPIF0, EXTI->FPR1, + "stale button EXTI0 pending must be cleared"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(PWR_SCR_CWUF, PWR->SCR, + "stale PWR wake flags must be cleared"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, + utest_nvic_get_clear_pending_count(EXTI0_1_IRQn), + "stale EXTI0_1 NVIC pending must be cleared before sleep"); + + // IWDG кормится и перед сном (E1), и сразу после (W1). + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(2, utest_watchdog_reload_count(), + "IWDG must be fed both before and after the sleep"); + + // Тактирование/периферия восстановлены после Stop. + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, utest_rcc_restore_count(), + "64 MHz clock must be restored after Stop"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, utest_systick_init_count(), + "systick must be re-inited after Stop"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, utest_adc_init_count(), + "ADC must be re-inited after Stop"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, utest_vmon_settle_count(), + "vmon settle window must be re-armed after Stop"); +} + +// ---- BLOCKER-регрессия: ISR будильника снимает ALRAIE, СОХРАНЯЯ ALRAF ---- +// Прямая линия EXTI19 = ALRAF & ALRAIE. При сработавшем будильнике ISR ОБЯЗАН +// снять ALRAIE (иначе линия висит и NVIC вечно tail-chain'ит RTC_TAMP_IRQ, а +// запрошенное Linux пробуждение по будильнику не доходит до главного цикла — +// именно этот отказ ронял Standby). При этом ALRAF чистить НЕЛЬЗЯ: его защёлку +// читает опросчик rtc_alarm_do_periodic_work. Снятие ALRAIE идёт через +// WPR-разблокированный помощник (голая запись RTC_CR под заблокированной WPR — +// no-op на железе). +static void test_stop_isr_alarm_masks_alraie_preserves_alraf(void) +{ + // Будильник вооружён (ALRAIE) и сработал (ALRAF защёлкнут). + RTC->CR = RTC_CR_ALRAIE; + RTC->SR = RTC_SR_ALRAF; + RTC->SCR = 0; + + mcu_stop_window_prepare(); + utest_irq_handler_t rtc_isr = utest_nvic_get_handler(RTC_TAMP_IRQn); + TEST_ASSERT_NOT_NULL(rtc_isr); + rtc_isr(); // имитируем срабатывание RTC_TAMP_IRQ на пробуждении + + TEST_ASSERT_FALSE_MESSAGE(RTC->CR & RTC_CR_ALRAIE, + "ISR must mask ALRAIE to dismiss the direct EXTI19 line"); + TEST_ASSERT_TRUE_MESSAGE(RTC->SR & RTC_SR_ALRAF, + "ISR must PRESERVE ALRAF for the poller (never CALRAF)"); + TEST_ASSERT_FALSE_MESSAGE(RTC->SCR & RTC_SCR_CALRAF, + "ISR must never write CALRAF"); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, utest_rtc_mask_alarm_irq_calls(), + "ALRAIE must be cleared via the WPR-unlocked rtc helper, not a bare CR write"); + TEST_ASSERT_TRUE_MESSAGE(utest_rtc_mask_alarm_irq_wpr_was_unlocked(), + "WPR must be unlocked around the CR write (else HW drops it)"); + + // Ни тик WUT, ни кнопка не должны взводиться будильниковым срабатыванием. + TEST_ASSERT_FALSE_MESSAGE(mcu_stop_take_wut_tick(), + "an alarm-only wake must not report a WUT tick"); + TEST_ASSERT_FALSE_MESSAGE(mcu_stop_take_button_wake(), + "an alarm-only wake must not report a button wake"); +} + +// ---- ISR тика WUT: чистит только WUTF, не трогает будильник ---- +static void test_stop_isr_wut_tick_clears_wutf_only(void) +{ + RTC->CR = RTC_CR_ALRAIE; // будильник вооружён, но не сработал + RTC->SR = RTC_SR_WUTF; // истёк период WUT + RTC->SCR = 0; + + mcu_stop_window_prepare(); + utest_irq_handler_t rtc_isr = utest_nvic_get_handler(RTC_TAMP_IRQn); + TEST_ASSERT_NOT_NULL(rtc_isr); + rtc_isr(); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(RTC_SCR_CWUTF, RTC->SCR, + "WUT tick must clear WUTF via SCR"); + TEST_ASSERT_TRUE_MESSAGE(RTC->CR & RTC_CR_ALRAIE, + "WUT tick must not disturb the armed alarm (ALRAIE)"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, utest_rtc_mask_alarm_irq_calls(), + "WUT tick must not touch the alarm irq helper"); + TEST_ASSERT_TRUE_MESSAGE(mcu_stop_take_wut_tick(), + "WUT tick must be reported to the deadline accumulator"); + TEST_ASSERT_FALSE_MESSAGE(mcu_stop_take_button_wake(), + "WUT tick must not report a button wake"); +} + +// ---- ISR кнопки (EXTI0): взводит button-wake, снимает FPR0 ---- +static void test_stop_isr_button_edge_sets_pending(void) +{ + EXTI->FPR1 = EXTI_FPR1_FPIF0; // защёлкнут падающий фронт PA0 + + mcu_stop_window_prepare(); + utest_irq_handler_t exti_isr = utest_nvic_get_handler(EXTI0_1_IRQn); + TEST_ASSERT_NOT_NULL(exti_isr); + exti_isr(); + + // Обработчик выполняет write-1-to-clear FPR0 (на железе снимает флаг). + TEST_ASSERT_EQUAL_UINT32_MESSAGE(EXTI_FPR1_FPIF0, EXTI->FPR1, + "button ISR must write-1-to-clear the FPR0 pending flag"); + TEST_ASSERT_TRUE_MESSAGE(mcu_stop_take_button_wake(), + "button edge must be reported as a button wake"); + TEST_ASSERT_FALSE_MESSAGE(mcu_stop_take_wut_tick(), + "button edge must not report a WUT tick"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, utest_rtc_mask_alarm_irq_calls(), + "button edge must not touch the alarm irq helper"); +} + +// ---- Выход окна: восстановление масок EXTI и выключение NVIC ---- +static void test_stop_finish_restores_masks(void) +{ + EXTI->IMR1 = EXTI_IMR1_IM9; // как до окна + mcu_stop_window_prepare(); // размаскирует IM0/IM19, замаскирует IM9 + mcu_stop_window_finish(); + + TEST_ASSERT_FALSE_MESSAGE(EXTI->IMR1 & (EXTI_IMR1_IM0 | EXTI_IMR1_IM19), + "finish must re-mask the Stop wake lines (EXTI0, EXTI19)"); + TEST_ASSERT_TRUE_MESSAGE(EXTI->IMR1 & EXTI_IMR1_IM9, + "finish must restore the SoC-CS mask (EXTI9)"); + TEST_ASSERT_FALSE_MESSAGE(utest_nvic_is_enabled(EXTI0_1_IRQn), + "finish must disable the button NVIC line"); + TEST_ASSERT_FALSE_MESSAGE(utest_nvic_is_enabled(RTC_TAMP_IRQn), + "finish must disable the RTC NVIC line"); + TEST_ASSERT_GREATER_OR_EQUAL_UINT32_MESSAGE(1, + utest_nvic_get_disable_count(RTC_TAMP_IRQn), + "finish must actually issue NVIC_DisableIRQ for RTC"); + TEST_ASSERT_TRUE_MESSAGE(utest_nvic_is_enabled(SPI2_IRQn), + "finish must re-enable the SPI2 NVIC line (regmap comms restored)"); +} + +// ---- Два окна подряд: кнопка обязана работать после окна, завершённого +// будильником (стенд 2026-07-09: «кнопка молчит во втором окне») ---- +// Окно 1: prepare -> будильник срабатывает в Stop (ISR) -> teardown (finish). +// Окно 2: prepare обязан ПОЛНОСТЬЮ перевооружить кнопочный путь (IMR0, FTSR0, +// NVIC, обработчик), и фронт кнопки обязан доехать до классификатора. +static void test_stop_two_windows_button_works_after_alarm_wake(void) +{ + // ---- Окно 1 ---- + EXTI->IMR1 = EXTI_IMR1_IM9; // как до окна (spi_slave_init) + mcu_stop_window_prepare(); + + RTC->CR = RTC_CR_ALRAIE; // будильник вооружён (rtc_set_alarm) + RTC->SR = RTC_SR_ALRAF; // и сработал в Stop + utest_irq_handler_t rtc_isr = utest_nvic_get_handler(RTC_TAMP_IRQn); + TEST_ASSERT_NOT_NULL(rtc_isr); + rtc_isr(); // пробуждение по будильнику + + mcu_stop_window_finish(); // выход окна по реальному пробуждению + TEST_ASSERT_FALSE_MESSAGE(EXTI->IMR1 & EXTI_IMR1_IM0, + "window 1 teardown must disarm the button line"); + TEST_ASSERT_FALSE_MESSAGE(utest_nvic_is_enabled(EXTI0_1_IRQn), + "window 1 teardown must disable the button NVIC line"); + + // ---- Окно 2 ---- + mcu_stop_window_prepare(); + + TEST_ASSERT_TRUE_MESSAGE(EXTI->IMR1 & EXTI_IMR1_IM0, + "window 2 must re-unmask the button line (EXTI0)"); + TEST_ASSERT_TRUE_MESSAGE(EXTI->FTSR1 & EXTI_FTSR1_FT0, + "window 2 must keep the falling-edge trigger on PA0"); + TEST_ASSERT_TRUE_MESSAGE(utest_nvic_is_enabled(EXTI0_1_IRQn), + "window 2 must re-enable the button NVIC line"); + utest_irq_handler_t exti_isr = utest_nvic_get_handler(EXTI0_1_IRQn); + TEST_ASSERT_NOT_NULL_MESSAGE(exti_isr, + "window 2 must have the button ISR registered"); + + // Фронт кнопки в окне 2 обязан дойти до классификатора. + EXTI->FPR1 = EXTI_FPR1_FPIF0; + exti_isr(); + TEST_ASSERT_TRUE_MESSAGE(mcu_stop_take_button_wake(), + "a button edge in window 2 must be reported as a button wake"); + + // И остатки окна 1 не должны протекать в окно 2. + TEST_ASSERT_FALSE_MESSAGE(mcu_stop_take_wut_tick(), + "no stale WUT tick may leak into window 2"); +} + +int main(void) +{ + UNITY_BEGIN(); + RUN_TEST(test_stop_prepare_arms_exti_and_nvic); + RUN_TEST(test_stop_enter_writes_stop1_registers); + RUN_TEST(test_stop_isr_alarm_masks_alraie_preserves_alraf); + RUN_TEST(test_stop_isr_wut_tick_clears_wutf_only); + RUN_TEST(test_stop_isr_button_edge_sets_pending); + RUN_TEST(test_stop_finish_restores_masks); + RUN_TEST(test_stop_two_windows_button_works_after_alarm_wake); + return UNITY_END(); +} diff --git a/unittests/mcu-pwr/utest_mcu_reg_env.c b/unittests/mcu-pwr/utest_mcu_reg_env.c new file mode 100644 index 0000000..c7dae61 --- /dev/null +++ b/unittests/mcu-pwr/utest_mcu_reg_env.c @@ -0,0 +1,152 @@ +#include "utest_mcu_reg_env.h" +#include "rtc.h" +#include "rcc.h" +#include "systick.h" +#include "adc.h" +#include "voltage-monitor.h" +#include "wdt-stm32.h" +#include + +// ---- Экземпляры регистров, живущие в ОЗУ теста ---- +PWR_TypeDef _utest_pwr; +RTC_TypeDef _utest_rtc; +EXTI_TypeDef _utest_exti; +SCB_TypeDef _utest_scb; + +utest_irq_handler_t _utest_nvic_handler[UTEST_IRQn_COUNT]; + +// ---- Внутреннее состояние окружения ---- +static struct { + bool nvic_enabled[UTEST_IRQn_COUNT]; + uint32_t nvic_disable_count[UTEST_IRQn_COUNT]; + uint32_t nvic_clear_pending_count[UTEST_IRQn_COUNT]; + + uint32_t wfi_count; + uint32_t wfi_scb_scr; + uint32_t wfi_pwr_cr1; + + uint32_t rtc_mask_alarm_irq_calls; + bool rtc_mask_alarm_irq_wpr_was_unlocked; + uint32_t rtc_periodic_wakeup_period; + + uint32_t rcc_restore_count; + uint32_t systick_init_count; + uint32_t adc_init_count; + uint32_t vmon_settle_count; + uint32_t watchdog_reload_count; +} env; + +void utest_reg_env_reset(void) +{ + memset(&_utest_pwr, 0, sizeof(_utest_pwr)); + memset(&_utest_rtc, 0, sizeof(_utest_rtc)); + memset(&_utest_exti, 0, sizeof(_utest_exti)); + memset(&_utest_scb, 0, sizeof(_utest_scb)); + memset(_utest_nvic_handler, 0, sizeof(_utest_nvic_handler)); + memset(&env, 0, sizeof(env)); +} + +// ---- NVIC / интринсики ---- +void NVIC_EnableIRQ(IRQn_Type irqn) +{ + if ((unsigned)irqn < UTEST_IRQn_COUNT) { + env.nvic_enabled[irqn] = true; + } +} + +void NVIC_DisableIRQ(IRQn_Type irqn) +{ + if ((unsigned)irqn < UTEST_IRQn_COUNT) { + env.nvic_enabled[irqn] = false; + env.nvic_disable_count[irqn]++; + } +} + +void NVIC_ClearPendingIRQ(IRQn_Type irqn) +{ + if ((unsigned)irqn < UTEST_IRQn_COUNT) { + env.nvic_clear_pending_count[irqn]++; + } +} + +void utest_stop_dsb(void) {} + +void utest_stop_wfi(void) +{ + // На железе WFI блокируется до пробуждения; в тесте снимаем состояние + // регистров в момент сна (SLEEPDEEP снимается уже в W-фазе) и возвращаемся. + env.wfi_scb_scr = _utest_scb.SCR; + env.wfi_pwr_cr1 = _utest_pwr.CR1; + env.wfi_count++; +} + +// ---- Заглушки внешних функций, вызываемых mcu-pwr.c ---- + +// Помощник rtc.c: моделируем защиту записи RTC_CR через WPR так же, как железо — +// снятие ALRAIE проходит, только если WPR разблокирована ключом 0xCA/0x53. +void rtc_mask_alarm_irq(void) +{ + env.rtc_mask_alarm_irq_calls++; + _utest_rtc.WPR = 0xCA; + _utest_rtc.WPR = 0x53; + env.rtc_mask_alarm_irq_wpr_was_unlocked = true; // WPR открыта на момент записи + _utest_rtc.CR &= ~RTC_CR_ALRAIE; + _utest_rtc.WPR = 0; +} + +void rtc_set_periodic_wakeup(uint16_t period_s) +{ + env.rtc_periodic_wakeup_period = period_s; +} + +void rtc_save_to_tamper_reg(uint8_t index, uint32_t data) { (void)index; (void)data; } +uint32_t rtc_get_tamper_reg(uint8_t index) { (void)index; return 0; } + +void rcc_set_hsi_pll_64mhz_clock(void) { env.rcc_restore_count++; } +void systick_init(void) { env.systick_init_count++; } +void adc_init(enum adc_clock clock_divider, enum adc_vref vref) +{ + (void)clock_divider; + (void)vref; + env.adc_init_count++; +} +void vmon_suspend_rearm_settle(void) { env.vmon_settle_count++; } +void watchdog_reload(void) { env.watchdog_reload_count++; } + +// ---- Аксессоры для теста ---- +utest_irq_handler_t utest_nvic_get_handler(IRQn_Type irqn) +{ + if ((unsigned)irqn < UTEST_IRQn_COUNT) { + return _utest_nvic_handler[irqn]; + } + return NULL; +} + +bool utest_nvic_is_enabled(IRQn_Type irqn) +{ + return ((unsigned)irqn < UTEST_IRQn_COUNT) ? env.nvic_enabled[irqn] : false; +} + +uint32_t utest_nvic_get_disable_count(IRQn_Type irqn) +{ + return ((unsigned)irqn < UTEST_IRQn_COUNT) ? env.nvic_disable_count[irqn] : 0; +} + +uint32_t utest_nvic_get_clear_pending_count(IRQn_Type irqn) +{ + return ((unsigned)irqn < UTEST_IRQn_COUNT) ? env.nvic_clear_pending_count[irqn] : 0; +} + +uint32_t utest_wfi_count(void) { return env.wfi_count; } +uint32_t utest_wfi_scb_scr(void) { return env.wfi_scb_scr; } +uint32_t utest_wfi_pwr_cr1(void) { return env.wfi_pwr_cr1; } + +uint32_t utest_rtc_mask_alarm_irq_calls(void) { return env.rtc_mask_alarm_irq_calls; } +bool utest_rtc_mask_alarm_irq_wpr_was_unlocked(void) { return env.rtc_mask_alarm_irq_wpr_was_unlocked; } +uint32_t utest_rtc_periodic_wakeup_period(void) { return env.rtc_periodic_wakeup_period; } + +uint32_t utest_rcc_restore_count(void) { return env.rcc_restore_count; } +uint32_t utest_systick_init_count(void) { return env.systick_init_count; } +uint32_t utest_adc_init_count(void) { return env.adc_init_count; } +uint32_t utest_vmon_settle_count(void) { return env.vmon_settle_count; } +uint32_t utest_watchdog_reload_count(void) { return env.watchdog_reload_count; } diff --git a/unittests/mcu-pwr/utest_mcu_reg_env.h b/unittests/mcu-pwr/utest_mcu_reg_env.h new file mode 100644 index 0000000..93b048b --- /dev/null +++ b/unittests/mcu-pwr/utest_mcu_reg_env.h @@ -0,0 +1,41 @@ +#pragma once +// Тестовое окружение register-stub для РЕАЛЬНОГО src/mcu-pwr.c. +// Даёт доступ к перехваченным NVIC/WFI и к счётчикам заглушек внешних функций. +#include +#include +#include "wbmcu_system.h" + +// Полный сброс: обнуляет регистры RTC/EXTI/PWR/SCB, NVIC, счётчики и снимок WFI. +void utest_reg_env_reset(void); + +// Возвращает обработчик, зарегистрированный mcu_stop_window_prepare() через +// NVIC_SetHandler (для прямого вызова ISR из теста). NULL, если не задан. +utest_irq_handler_t utest_nvic_get_handler(IRQn_Type irqn); + +// Состояние NVIC (взводится NVIC_EnableIRQ/DisableIRQ/ClearPendingIRQ). +bool utest_nvic_is_enabled(IRQn_Type irqn); +uint32_t utest_nvic_get_disable_count(IRQn_Type irqn); +uint32_t utest_nvic_get_clear_pending_count(IRQn_Type irqn); + +// Снимок регистров в момент __WFI() (SLEEPDEEP снимается уже ПОСЛЕ пробуждения, +// поэтому проверять его нужно именно на снимке момента сна). +uint32_t utest_wfi_count(void); +uint32_t utest_wfi_scb_scr(void); // SCB->SCR на момент WFI +uint32_t utest_wfi_pwr_cr1(void); // PWR->CR1 на момент WFI + +// Сколько раз ISR вызвал WPR-разблокированный помощник rtc_mask_alarm_irq() +// (доказывает, что снятие ALRAIE идёт через WPR-путь, а не голой записью CR). +uint32_t utest_rtc_mask_alarm_irq_calls(void); +// Была ли WPR разблокирована на момент записи CR внутри помощника +// (моделирует защиту записи RTC_CR: голая запись без разблокировки — no-op). +bool utest_rtc_mask_alarm_irq_wpr_was_unlocked(void); + +// Последний период, запрошенный rtc_set_periodic_wakeup() (mcu_goto_standby). +uint32_t utest_rtc_periodic_wakeup_period(void); + +// Счётчики восстановления после Stop (mcu_stop_enter W-фаза). +uint32_t utest_rcc_restore_count(void); +uint32_t utest_systick_init_count(void); +uint32_t utest_adc_init_count(void); +uint32_t utest_vmon_settle_count(void); +uint32_t utest_watchdog_reload_count(void); diff --git a/unittests/mcu-pwr/wbmcu_system.h b/unittests/mcu-pwr/wbmcu_system.h new file mode 100644 index 0000000..20e965c --- /dev/null +++ b/unittests/mcu-pwr/wbmcu_system.h @@ -0,0 +1,133 @@ +#pragma once +// Локальный register-stub заголовок для теста mcu-pwr. +// +// Заменяет device-header реального билда (system/include/wbmcu_system.h -> +// stm32g030xx.h): даёт РЕАЛЬНОМУ src/mcu-pwr.c ровно те регистры, битовые маски, +// NVIC-обёртки и интринсики, которыми он пользуется, но перенаправляет +// экземпляры RTC/EXTI/PWR/SCB на обычную ОЗУ, которой управляет тест. Значения +// битовых масок скопированы дословно из system/include/stm32g030xx.h / +// system/cmsis/core_cm0plus.h, чтобы поведение совпадало с железом. +#include +#include +#include + +// ---- Номера прерываний (stm32g030xx.h) ---- +typedef enum { + RTC_TAMP_IRQn = 2, // RTC через линию EXTI 19 + EXTI0_1_IRQn = 5, // EXTI 0 и 1 + SPI2_IRQn = 26, // SPI2 (regmap-движок; глушится на окно сна) + UTEST_IRQn_COUNT = 32, +} IRQn_Type; + +// ---- Периферийные структуры (только используемые поля) ---- +typedef struct { + volatile uint32_t CR1; + volatile uint32_t SR1; + volatile uint32_t SCR; +} PWR_TypeDef; + +typedef struct { + volatile uint32_t CR; + volatile uint32_t SR; + volatile uint32_t SCR; + volatile uint32_t WPR; +} RTC_TypeDef; + +typedef struct { + volatile uint32_t IMR1; + volatile uint32_t FTSR1; + volatile uint32_t FPR1; +} EXTI_TypeDef; + +typedef struct { + volatile uint32_t SCR; +} SCB_TypeDef; + +extern PWR_TypeDef _utest_pwr; +extern RTC_TypeDef _utest_rtc; +extern EXTI_TypeDef _utest_exti; +extern SCB_TypeDef _utest_scb; + +#define PWR (&_utest_pwr) +#define RTC (&_utest_rtc) +#define EXTI (&_utest_exti) +#define SCB (&_utest_scb) + +// ---- NVIC + интринсики (перехватываются тестом) ---- +typedef void (*utest_irq_handler_t)(void); +extern utest_irq_handler_t _utest_nvic_handler[UTEST_IRQn_COUNT]; + +// NVIC_SetHandler в реальном билде пишет в vector_table[]; здесь запоминаем +// обработчик, чтобы тест мог вызвать ISR напрямую. +#define NVIC_SetHandler(irqn, handler) (_utest_nvic_handler[(irqn)] = (handler)) + +void NVIC_EnableIRQ(IRQn_Type irqn); +void NVIC_DisableIRQ(IRQn_Type irqn); +void NVIC_ClearPendingIRQ(IRQn_Type irqn); + +void utest_stop_dsb(void); +void utest_stop_wfi(void); +#define __DSB() utest_stop_dsb() +#define __WFI() utest_stop_wfi() + +// ---- Битовые маски (дословно из stm32g030xx.h / core_cm0plus.h) ---- +#define PWR_SR1_WUF1_Pos (0U) +#define PWR_SR1_SBF_Pos (8U) +#define PWR_SR1_SBF_Msk (0x1UL << PWR_SR1_SBF_Pos) // 0x00000100 +#define PWR_SR1_SBF PWR_SR1_SBF_Msk +#define PWR_SR1_WUFI_Pos (15U) +#define PWR_SR1_WUFI_Msk (0x1UL << PWR_SR1_WUFI_Pos) // 0x00008000 +#define PWR_SR1_WUFI PWR_SR1_WUFI_Msk + +#define PWR_SCR_CWUF_Pos (0U) +#define PWR_SCR_CWUF_Msk (0x2BUL << PWR_SCR_CWUF_Pos) // 0x0000002B +#define PWR_SCR_CWUF PWR_SCR_CWUF_Msk +#define PWR_SCR_CWUF1_Pos (0U) +#define PWR_SCR_CWUF1_Msk (0x1UL << PWR_SCR_CWUF1_Pos) // 0x00000001 +#define PWR_SCR_CWUF1 PWR_SCR_CWUF1_Msk +#define PWR_SCR_CSBF_Pos (8U) +#define PWR_SCR_CSBF_Msk (0x1UL << PWR_SCR_CSBF_Pos) // 0x00000100 +#define PWR_SCR_CSBF PWR_SCR_CSBF_Msk + +#define PWR_CR1_LPMS_Pos (0U) +#define PWR_CR1_LPMS_Msk (0x7UL << PWR_CR1_LPMS_Pos) // 0x00000007 +#define PWR_CR1_LPMS PWR_CR1_LPMS_Msk +#define PWR_CR1_LPMS_0 (0x1UL << PWR_CR1_LPMS_Pos) // 0x00000001 +#define PWR_CR1_LPMS_1 (0x2UL << PWR_CR1_LPMS_Pos) // 0x00000002 + +#define RTC_CR_ALRAIE_Pos (12U) +#define RTC_CR_ALRAIE_Msk (0x1UL << RTC_CR_ALRAIE_Pos) // 0x00001000 +#define RTC_CR_ALRAIE RTC_CR_ALRAIE_Msk + +#define RTC_SR_WUTF_Pos (2U) +#define RTC_SR_WUTF_Msk (0x1UL << RTC_SR_WUTF_Pos) // 0x00000004 +#define RTC_SR_WUTF RTC_SR_WUTF_Msk +#define RTC_SR_ALRAF_Pos (0U) +#define RTC_SR_ALRAF_Msk (0x1UL << RTC_SR_ALRAF_Pos) // 0x00000001 +#define RTC_SR_ALRAF RTC_SR_ALRAF_Msk + +#define RTC_SCR_CWUTF_Pos (2U) +#define RTC_SCR_CWUTF_Msk (0x1UL << RTC_SCR_CWUTF_Pos) // 0x00000004 +#define RTC_SCR_CWUTF RTC_SCR_CWUTF_Msk +#define RTC_SCR_CALRAF_Pos (0U) +#define RTC_SCR_CALRAF_Msk (0x1UL << RTC_SCR_CALRAF_Pos) // 0x00000001 +#define RTC_SCR_CALRAF RTC_SCR_CALRAF_Msk + +#define EXTI_FTSR1_FT0_Pos (0U) +#define EXTI_FTSR1_FT0_Msk (0x1UL << EXTI_FTSR1_FT0_Pos) // 0x00000001 +#define EXTI_FTSR1_FT0 EXTI_FTSR1_FT0_Msk +#define EXTI_FPR1_FPIF0_Pos (0U) +#define EXTI_FPR1_FPIF0_Msk (0x1UL << EXTI_FPR1_FPIF0_Pos) // 0x00000001 +#define EXTI_FPR1_FPIF0 EXTI_FPR1_FPIF0_Msk +#define EXTI_IMR1_IM0_Pos (0U) +#define EXTI_IMR1_IM0_Msk (0x1UL << EXTI_IMR1_IM0_Pos) // 0x00000001 +#define EXTI_IMR1_IM0 EXTI_IMR1_IM0_Msk +#define EXTI_IMR1_IM9_Pos (9U) +#define EXTI_IMR1_IM9_Msk (0x1UL << EXTI_IMR1_IM9_Pos) // 0x00000200 +#define EXTI_IMR1_IM9 EXTI_IMR1_IM9_Msk +#define EXTI_IMR1_IM19_Pos (19U) +#define EXTI_IMR1_IM19_Msk (0x1UL << EXTI_IMR1_IM19_Pos) // 0x00080000 +#define EXTI_IMR1_IM19 EXTI_IMR1_IM19_Msk + +#define SCB_SCR_SLEEPDEEP_Pos (2U) +#define SCB_SCR_SLEEPDEEP_Msk (1UL << SCB_SCR_SLEEPDEEP_Pos) // 0x00000004 diff --git a/unittests/utest_helpers/mcu-pwr/utest_mcu_pwr.c b/unittests/utest_helpers/mcu-pwr/utest_mcu_pwr.c index ca8ab7b..150e253 100644 --- a/unittests/utest_helpers/mcu-pwr/utest_mcu_pwr.c +++ b/unittests/utest_helpers/mcu-pwr/utest_mcu_pwr.c @@ -6,6 +6,14 @@ static struct { enum mcu_vcc_5v_state vcc_5v_state; bool init_called; uint16_t standby_wakeup_time; + + // --- suspend-to-off Stop 1 --- + bool stop_window_prepared; + bool stop_window_finished; + uint32_t stop_enter_count; + bool stop_auto_wut_tick; // каждый mcu_stop_enter даёт тик WUT + bool stop_wut_tick_pending; + bool stop_button_wake_pending; } mcu_state = { .poweron_reason = MCU_POWERON_REASON_POWER_ON, .vcc_5v_state = MCU_VCC_5V_STATE_OFF, @@ -29,6 +37,41 @@ void mcu_goto_standby(uint16_t wakeup_after_s) mcu_state.standby_wakeup_time = wakeup_after_s; } +void mcu_stop_window_prepare(void) +{ + mcu_state.stop_window_prepared = true; +} + +void mcu_stop_window_finish(void) +{ + mcu_state.stop_window_finished = true; +} + +void mcu_stop_enter(void) +{ + // На железе блокируется в WFI до пробуждения; в тесте возвращаемся сразу. + // Тик WUT НЕ взводим по умолчанию (иначе дедлайн срабатывал бы каждый + // проход); при stop_auto_wut_tick моделируем один истёкший период WUT. + mcu_state.stop_enter_count++; + if (mcu_state.stop_auto_wut_tick) { + mcu_state.stop_wut_tick_pending = true; + } +} + +bool mcu_stop_take_wut_tick(void) +{ + bool ret = mcu_state.stop_wut_tick_pending; + mcu_state.stop_wut_tick_pending = false; + return ret; +} + +bool mcu_stop_take_button_wake(void) +{ + bool ret = mcu_state.stop_button_wake_pending; + mcu_state.stop_button_wake_pending = false; + return ret; +} + enum mcu_vcc_5v_state mcu_get_vcc_5v_last_state(void) { return mcu_state.vcc_5v_state; @@ -60,10 +103,46 @@ uint16_t utest_mcu_get_standby_wakeup_time(void) return mcu_state.standby_wakeup_time; } +void utest_mcu_stop_set_auto_wut_tick(bool on) +{ + mcu_state.stop_auto_wut_tick = on; +} + +void utest_mcu_stop_inject_wut_tick(void) +{ + mcu_state.stop_wut_tick_pending = true; +} + +void utest_mcu_stop_set_button_wake(bool pending) +{ + mcu_state.stop_button_wake_pending = pending; +} + +bool utest_mcu_stop_get_window_prepared(void) +{ + return mcu_state.stop_window_prepared; +} + +bool utest_mcu_stop_get_window_finished(void) +{ + return mcu_state.stop_window_finished; +} + +uint32_t utest_mcu_stop_get_enter_count(void) +{ + return mcu_state.stop_enter_count; +} + void utest_mcu_reset(void) { mcu_state.poweron_reason = MCU_POWERON_REASON_POWER_ON; mcu_state.vcc_5v_state = MCU_VCC_5V_STATE_OFF; mcu_state.init_called = false; mcu_state.standby_wakeup_time = 0; + mcu_state.stop_window_prepared = false; + mcu_state.stop_window_finished = false; + mcu_state.stop_enter_count = 0; + mcu_state.stop_auto_wut_tick = false; + mcu_state.stop_wut_tick_pending = false; + mcu_state.stop_button_wake_pending = false; } diff --git a/unittests/utest_helpers/mcu-pwr/utest_mcu_pwr.h b/unittests/utest_helpers/mcu-pwr/utest_mcu_pwr.h index 62753c1..0d54d70 100644 --- a/unittests/utest_helpers/mcu-pwr/utest_mcu_pwr.h +++ b/unittests/utest_helpers/mcu-pwr/utest_mcu_pwr.h @@ -16,5 +16,19 @@ bool utest_mcu_was_init_called(void); // Получить параметр wakeup_after_s последнего вызова mcu_goto_standby() uint16_t utest_mcu_get_standby_wakeup_time(void); +// --- suspend-to-off Stop 1 --- +// Если on = true, каждый mcu_stop_enter() моделирует один истёкший период WUT +// (mcu_stop_take_wut_tick() вернёт true) — так продвигается дедлайн по тикам. +void utest_mcu_stop_set_auto_wut_tick(bool on); +// Взвести одиночный тик WUT (следующий mcu_stop_take_wut_tick() вернёт true). +void utest_mcu_stop_inject_wut_tick(void); +// Смоделировать пробуждение по фронту кнопки (mcu_stop_take_button_wake()). +void utest_mcu_stop_set_button_wake(bool pending); +// Был ли вызван mcu_stop_window_prepare() / _finish(). +bool utest_mcu_stop_get_window_prepared(void); +bool utest_mcu_stop_get_window_finished(void); +// Сколько раз EC уходил в Stop (вызовов mcu_stop_enter()). +uint32_t utest_mcu_stop_get_enter_count(void); + // Сбросить состояние мока void utest_mcu_reset(void); diff --git a/unittests/utest_helpers/pwrkey/utest_pwrkey.c b/unittests/utest_helpers/pwrkey/utest_pwrkey.c index 9fc44a1..76bd251 100644 --- a/unittests/utest_helpers/pwrkey/utest_pwrkey.c +++ b/unittests/utest_helpers/pwrkey/utest_pwrkey.c @@ -1,4 +1,6 @@ #include "utest_pwrkey.h" +#include "config.h" +#include "systick.h" static bool pwrkey_long_press = false; static bool pwrkey_short_press = false; @@ -6,6 +8,22 @@ static bool pwrkey_is_pressed = false; static bool pwrkey_is_ready = true; static uint32_t pwrkey_periodic_work_call_count = 0; +// --- Опциональная модель антидребезга (зеркалит src/pwrkey.c) --- +// По умолчанию выключена: pwrkey_do_periodic_work() остаётся no-op-счётчиком, +// как раньше, чтобы не менять поведение существующих тестов. +enum dbnc_state { + DBNC_UNINIT = 0, + DBNC_RELEASED, + DBNC_PRESSED, +}; + +static bool dbnc_model_enabled = false; +static enum dbnc_state dbnc_prev_raw; // предыдущее сырое состояние +static systime_t dbnc_raw_change_ts; // когда сырое состояние изменилось +static enum dbnc_state dbnc_logic; // состояние после антидребезга +static enum dbnc_state dbnc_prev_logic; // для детектирования фронтов +static bool dbnc_press_begun; // было начато нажатие + void utest_pwrkey_reset(void) { pwrkey_long_press = false; @@ -13,6 +31,13 @@ void utest_pwrkey_reset(void) pwrkey_is_pressed = false; pwrkey_is_ready = true; pwrkey_periodic_work_call_count = 0; + + dbnc_model_enabled = false; + dbnc_prev_raw = DBNC_UNINIT; + dbnc_raw_change_ts = 0; + dbnc_logic = DBNC_UNINIT; + dbnc_prev_logic = DBNC_UNINIT; + dbnc_press_begun = false; } void utest_set_pwrkey_long_press(bool value) @@ -40,6 +65,11 @@ uint32_t utest_pwrkey_get_periodic_work_call_count(void) return pwrkey_periodic_work_call_count; } +void utest_pwrkey_enable_debounce_model(bool on) +{ + dbnc_model_enabled = on; +} + bool pwrkey_handle_long_press(void) { bool ret = pwrkey_long_press; @@ -61,10 +91,52 @@ bool pwrkey_ready(void) bool pwrkey_pressed(void) { + // На железе pwrkey_pressed() возвращает АНТИДРЕБЕЗЖЕННЫЙ уровень + // (gpio_ctx.logic_state), а не сырой GPIO. С включённой моделью зеркалим + // это: уровень меняется через PWRKEY_DEBOUNCE_MS после сырого, и «короткое + // нажатие» фиксируется в том же проходе, в котором уровень становится + // released - инвариант, на который опирается «глотание» хвоста нажатия + // после окна сна. Раньше мок отдавал сырой уровень (mock gap). + if (dbnc_model_enabled) { + return dbnc_logic == DBNC_PRESSED; + } return pwrkey_is_pressed; } +static void dbnc_do_periodic_work(void) +{ + enum dbnc_state raw = pwrkey_is_pressed ? DBNC_PRESSED : DBNC_RELEASED; + + // Антидребезг: raw -> logic после > PWRKEY_DEBOUNCE_MS стабильности. + if (dbnc_prev_raw != raw) { + dbnc_prev_raw = raw; + dbnc_raw_change_ts = systick_get_system_time_ms(); + } + if (dbnc_logic != raw) { + if (systick_get_time_since_timestamp(dbnc_raw_change_ts) > PWRKEY_DEBOUNCE_MS) { + dbnc_logic = raw; + } + } + + // Детектирование нажатий по подтверждённому logic-состоянию. + if (dbnc_prev_logic != dbnc_logic) { + if (dbnc_prev_logic == DBNC_RELEASED && dbnc_logic == DBNC_PRESSED) { + dbnc_press_begun = true; + } + if (dbnc_prev_logic == DBNC_PRESSED && dbnc_logic == DBNC_RELEASED) { + if (dbnc_press_begun) { + pwrkey_short_press = true; // подтверждённое короткое нажатие + } + dbnc_press_begun = false; + } + dbnc_prev_logic = dbnc_logic; + } +} + void pwrkey_do_periodic_work(void) { pwrkey_periodic_work_call_count++; + if (dbnc_model_enabled) { + dbnc_do_periodic_work(); + } } diff --git a/unittests/utest_helpers/pwrkey/utest_pwrkey.h b/unittests/utest_helpers/pwrkey/utest_pwrkey.h index 290b953..ac7c57b 100644 --- a/unittests/utest_helpers/pwrkey/utest_pwrkey.h +++ b/unittests/utest_helpers/pwrkey/utest_pwrkey.h @@ -11,3 +11,13 @@ void utest_set_pwrkey_pressed(bool value); void utest_pwrkey_set_ready(bool ready); void utest_pwrkey_set_short_press(bool value); uint32_t utest_pwrkey_get_periodic_work_call_count(void); + +// Опциональная модель антидребезга (по умолчанию ВЫКЛЮЧЕНА — совместимо со +// всеми существующими тестами). Когда включена, pwrkey_do_periodic_work() +// прогоняет тот же фильтр, что и реальный pwrkey.c: raw-состояние +// (utest_set_pwrkey_pressed) превращается в подтверждённое короткое нажатие +// (pwrkey_handle_short_press) только после удержания > PWRKEY_DEBOUNCE_MS и +// последующего отпускания > PWRKEY_DEBOUNCE_MS, по системному времени +// (utest_systick). Позволяет прогнать нажатие end-to-end через +// pwrkey_do_periodic_work, а не инъекцией на границе мока. +void utest_pwrkey_enable_debounce_model(bool on); diff --git a/unittests/utest_helpers/rtc/utest_rtc.c b/unittests/utest_helpers/rtc/utest_rtc.c index 773e6cb..b536f01 100644 --- a/unittests/utest_helpers/rtc/utest_rtc.c +++ b/unittests/utest_helpers/rtc/utest_rtc.c @@ -10,6 +10,11 @@ static struct { bool alarm_flag_cleared; bool periodic_wakeup_disabled; + // Периодическое пробуждение (RTC WUT): в режиме Stop служит и тиком + // дедлайна, и кормлением IWDG. Запоминаем последний запрошенный период. + bool periodic_wakeup_set; + uint16_t periodic_wakeup_period_s; + // Track what was set bool datetime_was_set; struct rtc_time datetime_set_value; @@ -89,6 +94,14 @@ bool utest_rtc_get_periodic_wakeup_disabled(void) return rtc_state.periodic_wakeup_disabled; } +bool utest_rtc_get_periodic_wakeup_set(uint16_t * period_s) +{ + if (period_s != NULL && rtc_state.periodic_wakeup_set) { + *period_s = rtc_state.periodic_wakeup_period_s; + } + return rtc_state.periodic_wakeup_set; +} + // Мок-реализация RTC API void rtc_init(void) { @@ -161,6 +174,10 @@ void rtc_enable_pc13_1hz_clkout(void) {} void rtc_disable_pc13_1hz_clkout(void) {} void rtc_enable_pa4_1hz_clkout(void) {} void rtc_disable_pa4_1hz_clkout(void) {} -void rtc_set_periodic_wakeup(uint16_t period_s) { (void)period_s; } +void rtc_set_periodic_wakeup(uint16_t period_s) +{ + rtc_state.periodic_wakeup_set = true; + rtc_state.periodic_wakeup_period_s = period_s; +} void rtc_save_to_tamper_reg(uint8_t index, uint32_t data) { (void)index; (void)data; } uint32_t rtc_get_tamper_reg(uint8_t index) { (void)index; return 0; } diff --git a/unittests/utest_helpers/rtc/utest_rtc.h b/unittests/utest_helpers/rtc/utest_rtc.h index 4cdbb52..2a59408 100644 --- a/unittests/utest_helpers/rtc/utest_rtc.h +++ b/unittests/utest_helpers/rtc/utest_rtc.h @@ -17,3 +17,7 @@ bool utest_rtc_get_was_alarm_set(struct rtc_alarm * alarm); bool utest_rtc_get_was_offset_set(uint16_t * offset); bool utest_rtc_was_alarm_flag_cleared(void); bool utest_rtc_get_periodic_wakeup_disabled(void); + +// Возвращает true, если программировался период RTC WUT (rtc_set_periodic_wakeup); +// в *period_s кладёт последний запрошенный период в секундах. +bool utest_rtc_get_periodic_wakeup_set(uint16_t * period_s); diff --git a/unittests/wbec-integration/wbec_integration_test.c b/unittests/wbec-integration/wbec_integration_test.c index 8f37805..a794b70 100644 --- a/unittests/wbec-integration/wbec_integration_test.c +++ b/unittests/wbec-integration/wbec_integration_test.c @@ -20,6 +20,7 @@ #include "utest_voltage_monitor.h" #include "utest_wbmz_common.h" #include "utest_irq.h" +#include "utest_wdt_stm32.h" /** * Интеграционный тест: реальные wbec.c + linux-power-control.c + wdt.c @@ -138,8 +139,26 @@ bool rtc_alarm_take_fired(void) bool temperature_control_is_temperature_ready(void) { return true; } int16_t temperature_control_get_temperature_c_x100(void) { return 2500; } +// Управляемые супер-циклом выходы, переводимые в безопасное состояние на окно +// suspend-to-off. Отслеживаем текущее состояние заморозки для проверок. +static bool heater_suspend_off; +static bool v_out_suspend_off; + +void temperature_control_suspend(bool on) { heater_suspend_off = on; } +void gpio_suspend(bool on) { v_out_suspend_off = on; } + 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; } +// Пищалка: считаем бипы (бип подтверждения кнопочного пробуждения, бип +// включения на входе в WORKING). На WB74 пищалки нет (заглушка в buzzer.h +// съедает вызовы), поэтому проверки счётчика гейтятся на EC_GPIO_BUZZER. +static uint32_t buzzer_beep_count; +static uint16_t buzzer_last_beep_ms; +void buzzer_beep(uint16_t freq, uint16_t duration_ms) +{ + (void)freq; + buzzer_beep_count++; + buzzer_last_beep_ms = duration_ms; +} // ==================== Модель платы ==================== @@ -253,7 +272,11 @@ static void sim_tick(void) utest_vmon_set_ch_status(VMON_CHANNEL_V33, sim.v33); utest_vmon_set_ch_status(VMON_CHANNEL_V_IN, true); - // Периодические задачи в порядке main loop + // Периодические задачи в порядке main loop. pwrkey_do_periodic_work идёт + // первым, как в main.c: с включённой моделью антидребезга (по умолчанию + // выключена) он превращает удержание кнопки в подтверждённое короткое + // нажатие по системному времени — так же, как на железе. + pwrkey_do_periodic_work(); wdt_do_periodic_work(); linux_cpu_pwr_seq_do_periodic_work(); wbec_do_periodic_work(); @@ -318,6 +341,8 @@ void utest_wdt_module_reset_state(void); void setUp(void) { memset(&sim, 0, sizeof(sim)); + buzzer_beep_count = 0; + buzzer_last_beep_ms = 0; sim.check_invariants = true; working_entry_count = 0; last_working_entry_time = 0; @@ -336,6 +361,29 @@ void setUp(void) utest_pwrkey_reset(); utest_rtc_reset(); utest_irq_reset(); + heater_suspend_off = false; + v_out_suspend_off = false; +} + +// Причины включения из wbec.c (порядок enum linux_poweron_reason) +enum utest_wbec_poweron_reason { + UTEST_REASON_POWER_ON, + UTEST_REASON_POWER_KEY, + UTEST_REASON_RTC_ALARM, + UTEST_REASON_REBOOT, + UTEST_REASON_REBOOT_NO_ALARM, + UTEST_REASON_WATCHDOG, + UTEST_REASON_PMIC_OFF, + UTEST_REASON_UNKNOWN, + UTEST_REASON_WATCHDOG_WARM, +}; + +// poweron_reason пишется в regmap на каждом wbec_do_periodic_work. +static uint16_t get_poweron_reason_from_regmap(void) +{ + struct REGMAP_POWERON_REASON pr; + TEST_ASSERT_TRUE(utest_regmap_get_region_data(REGMAP_REGION_POWERON_REASON, &pr, sizeof(pr))); + return pr.poweron_reason; } void tearDown(void) @@ -705,6 +753,684 @@ static void test_suspend_off_second_cycle_sleeps_with_watchdog(void) "cycle 2 must still wake on a fresh alarm"); } +// Регрессия (стенд 2026-07-08, подозрение «залипший ALRAF»): два окна подряд, +// первое завершается СРАБОТАВШИМ будильником. Второе окно НЕ должно мгновенно +// классифицировать будильник на входе — защёлки прошлого цикла обязаны быть +// дренированы объявлением; будит только свежий будильник, и причина +// пробуждения — RTC_ALARM свежего события. +static void test_suspend_off_second_window_no_stale_alarm_at_entry(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 (запрос 60 с; дедлайн 60+10 с — далеко за границей проверки) + sim_announce_off_mode(60); + sim_tick(); + sim.pmic_crashed = true; // BL31 снял 3.3В + sim_run_ms(30000); // полминуты сна + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_after_c1, sim.soc_boot_count, + "Window 2 must NOT wake instantly on a stale alarm from window 1"); + + alarm_fired = true; // свежий будильник + sim.pmic_crashed = false; + sim_run_ms(3000); + + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_after_c1, + "Window 2 must wake on the fresh alarm"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_RTC_ALARM, get_poweron_reason_from_regmap(), + "Fresh-alarm wake must report REASON_RTC_ALARM"); +} + +// Фиксация поведения (стенд 2026-07-08): будильник, сработавший МЕЖДУ +// объявлением окна и реальным пропаданием 3.3В, обязан разбудить плату +// НЕМЕДЛЕННО при входе в окно — запрошенное время пробуждения уже прошло. +// На стенде это выглядело как «мгновенное пробуждение»: диагностический вход +// BL31 в suspend занимает ~80 с (подсчёт контрольных сумм DRAM), и запросы +// 30-60 с истекают ещё ДО начала сна. Это корректное поведение, унаследованное +// от busy-poll a655128; тест защищает его от «исправления». +static void test_suspend_off_alarm_fired_before_sleep_start_wakes_at_entry(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(30); + sim_tick(); // объявление дренирует УСТАРЕВШИЕ защёлки + + // Будильник срабатывает, пока 3.3В ещё присутствует (SoC долго готовится + // ко сну) — плата НЕ должна перезапускаться, пока сон не начался + sim_run_ms(2000); + alarm_fired = true; + sim_run_ms(1000); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_before, sim.soc_boot_count, + "Board must not restart while 3.3V is still up"); + + // 3.3В наконец пропало: окно открывается с уже сработавшим будильником + sim.pmic_crashed = true; + sim_run_ms(8000); // мгновенная классификация + пробуждение + + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_before, + "Entry with an already-fired alarm must wake the board immediately"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_RTC_ALARM, get_poweron_reason_from_regmap(), + "Pre-sleep-fired alarm must be reported as REASON_RTC_ALARM"); +} + +// ==================== Stop 1: окно сна suspend-to-off ==================== + +// Доводит плату до состояния «спит в окне suspend-to-off»: объявлен off-mode, +// 3.3В пропало (BL31), EC ушёл в STM32 Stop. Инварианты выключены: плата +// намеренно не стартует, пока не разбудят. +static void sim_enter_off_mode_sleep(uint16_t timeout_s) +{ + sim.check_invariants = false; + sim.soc_feeds = true; + sim_boot_to_working(); + // Как на реальной плате, suspend объявляется, когда Linux уже загружен: + // до этого момента короткое нажатие кнопки трактуется как немедленное + // выключение (ветка !linux_booted), а не как выход из suspend. + sim_run_ms(WBEC_LINUX_BOOT_TIME_MS + 1000); + sim_announce_off_mode(timeout_s); + sim_tick(); + sim.pmic_crashed = true; // BL31 снял 3.3В -> взводится suspend_started + sim_run_ms(500); +} + +// Вход в окно должен: завести источники пробуждения Stop, арм-нуть WUT ровно +// с периодом кормления, принудительно выключить нагреватель (PD2) и V_OUT +// (PD0), реально уснуть и НИ разу не уйти в Standby. Свежий будильник (запрос +// Linux) обязан разбудить тем же путём linux_cpu_pwr_seq_wakeup, вернуть +// выходы под управление, снять маску SoC-CS и выключить WUT-дедлайн. +static void test_stop_window_arms_safe_and_wakes_on_fresh_alarm(void) +{ + sim_enter_off_mode_sleep(60); + + TEST_ASSERT_TRUE_MESSAGE(utest_wbec_get_suspend_started(), + "3.3V drop must arm the sleep"); + TEST_ASSERT_TRUE_MESSAGE(utest_mcu_stop_get_window_prepared(), + "Stop window wake sources (RTC/​button EXTI) must be armed"); + uint16_t wut_period = 0; + TEST_ASSERT_TRUE_MESSAGE(utest_rtc_get_periodic_wakeup_set(&wut_period), + "the feed/​deadline WUT must be armed"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(WBEC_SUSPEND_STOP_FEED_PERIOD_S, wut_period, + "WUT must be armed with the feed period"); + TEST_ASSERT_TRUE_MESSAGE(heater_suspend_off, "heater (PD2) must be forced off at Stop entry"); + TEST_ASSERT_TRUE_MESSAGE(v_out_suspend_off, "V_OUT (PD0) must be forced off at Stop entry"); + TEST_ASSERT_TRUE_MESSAGE(utest_mcu_stop_get_enter_count() > 0, + "EC must actually enter Stop while asleep"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(0, utest_mcu_get_standby_wakeup_time(), + "the Stop window must never request Standby"); + + uint32_t boots_before = sim.soc_boot_count; + + alarm_fired = true; // свежий будильник; PMIC восстановит 3.3В + sim.pmic_crashed = false; + sim_run_ms(3000); + + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_before, + "a fresh alarm must wake the board"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_RTC_ALARM, get_poweron_reason_from_regmap(), + "alarm wake must report REASON_RTC_ALARM"); + TEST_ASSERT_TRUE_MESSAGE(utest_mcu_stop_get_window_finished(), + "real wake must restore the SoC-CS EXTI mask (window finish)"); + TEST_ASSERT_TRUE_MESSAGE(utest_rtc_get_periodic_wakeup_disabled(), + "real wake must disable the WUT deadline"); + TEST_ASSERT_FALSE_MESSAGE(heater_suspend_off, "heater control must be restored on wake"); + TEST_ASSERT_FALSE_MESSAGE(v_out_suspend_off, "V_OUT control must be restored on wake"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(0, utest_mcu_get_standby_wakeup_time(), + "no Standby must be requested across the whole window"); + +#if defined EC_GPIO_BUZZER + // Необслуживаемое пробуждение (будильник) обязано быть БЕЗЗВУЧНЫМ: единственный + // бип за тест - вход в WORKING на холодной загрузке. + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, buzzer_beep_count, + "an alarm wake must be silent (no wake-confirm beep, WORKING re-entry suppressed)"); +#endif +} + +// Кнопка PWRON, UX-контракт «нажал - услышал бип - отпустил»: фронт EXTI0 +// будит ядро, антидребезг (500 мс) подтверждает НАЖАТИЕ, и пробуждение +// стартует ПО ПОДТВЕРЖДЕНИЮ - ещё ДО отпускания. Бип звучит на возврате 3.3В +// (пищалка запитана от 3.3В), пока кнопка ещё в руке. Отпускание приходит +// после выхода из окна и глотается (не порождает событий для Linux). +// +// Нажатие прогоняется END-TO-END через реальный фильтр 500 мс по системному +// времени через pwrkey_do_periodic_work - не инъекцией готового short-press. +static void test_stop_wake_by_button_edge_then_debounced_press(void) +{ + // Модель антидребезга включена с самого начала — как на железе, где pwrkey + // крутится всё время и удерживает базовое состояние logic=RELEASED. + utest_pwrkey_enable_debounce_model(true); + sim_enter_off_mode_sleep(60); + uint32_t boots_before = sim.soc_boot_count; + + // Фронт кнопки будит Stop; кнопка физически удерживается. PMIC остаётся + // спящим (как на железе): его оживит только PWRON последовательности + // пробуждения (модель: удержание PWRON оживляет PMIC). + utest_mcu_stop_set_button_wake(true); + utest_set_pwrkey_pressed(true); + + // До подтверждения антидребезгом (< 500 мс) плата НЕ будится - фильтр + // случайных касаний сохранён. + sim_run_ms(PWRKEY_DEBOUNCE_MS - 100); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_before, sim.soc_boot_count, + "the board must not wake before the press is debounce-confirmed"); + + // Подтверждение (+500 мс) запускает пробуждение, КНОПКА ЕЩЁ УДЕРЖИВАЕТСЯ. + sim_run_ms(3000); + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_before, + "a confirmed press must wake the board BEFORE release"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_POWER_KEY, get_poweron_reason_from_regmap(), + "button wake must report REASON_POWER_KEY"); + +#if defined EC_GPIO_BUZZER + // Бип-подтверждение прозвучал, пока кнопку ещё держат (на возврате 3.3В). + // 1 бип загрузки (WORKING холодного старта) + 1 бип кнопочного пробуждения. + TEST_ASSERT_EQUAL_UINT32_MESSAGE(2, buzzer_beep_count, + "the wake-confirm beep must sound while the button is still held"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(EC_BUZZER_BEEP_SUSPEND_WAKE_MS, buzzer_last_beep_ms, + "the wake-confirm beep must be the short suspend-wake beep"); +#endif + + // Отпускание после resume: никаких событий кнопки для системы. + uint32_t boots_after_wake = sim.soc_boot_count; + utest_set_pwrkey_pressed(false); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 2000); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_after_wake, sim.soc_boot_count, + "the release must not restart or power off the board"); + TEST_ASSERT_TRUE_MESSAGE(utest_gpio_get_output_state(gpio_5v) != 0, + "the release must not power the board off"); +} + +// Регрессия (стенд 2026-07-08, кнопочный тест): одно нажатие = пробуждение + +// poweroff + cold boot. Пока кнопку держат в окне сна, обычный обработчик +// WORKING (linux_booted -> irq_set_flag(IRQ_PWR_OFF_REQ)) выставляет Linux'у +// «запрос выключения»; Linux спит и флаг не квитирует, и после resume драйвер +// wbec-pwrkey доставлял то же нажатие второй раз - logind выключал только что +// разбуженную плату. Нажатие, потреблённое как причина пробуждения, НЕ должно +// оставлять Linux'у отложенных событий кнопки. +static void test_stop_button_wake_press_not_redelivered_to_linux(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim_enter_off_mode_sleep(60); + uint32_t boots_before = sim.soc_boot_count; + + // Фронт кнопки будит Stop; кнопка удерживается; пробуждение стартует по + // подтверждению (+500 мс). Взведённый в том же такте IRQ_PWR_OFF_REQ + // (обычный обработчик WORKING) гасится выходом из окна в этом же проходе. + utest_mcu_stop_set_button_wake(true); + utest_set_pwrkey_pressed(true); + sim_run_ms(3000); + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_before, + "the confirmed press must wake the board"); + + // Отпускание после resume: событие обязано быть проглочено. + utest_set_pwrkey_pressed(false); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 3000); + + TEST_ASSERT_FALSE_MESSAGE(utest_irq_is_flag_set(IRQ_PWR_OFF_REQ), + "the wake press must NOT stay pending for Linux as a power-key event"); + TEST_ASSERT_TRUE_MESSAGE(utest_gpio_get_output_state(gpio_5v) != 0, + "the release must not power the board off"); + +#if defined EC_GPIO_BUZZER + // Пара требований UX (Evgeny, 2026-07-09): бип-подтверждение ЕСТЬ, + // poweroff-событие для Linux - НЕТ. + TEST_ASSERT_EQUAL_UINT32_MESSAGE(2, buzzer_beep_count, + "the button wake must beep (boot beep + wake-confirm beep)"); +#endif +} + +// Контроль от перекоррекции: нажатие в ОБЫЧНОЙ работе (Linux загружен, suspend +// не объявлен) по-прежнему доставляется в Linux как запрос выключения. +static void test_running_press_still_delivers_pwr_off_req(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim.soc_feeds = true; + sim_boot_to_working(); + sim_run_ms(WBEC_LINUX_BOOT_TIME_MS + 1000); // linux_booted = true + + utest_set_pwrkey_pressed(true); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 100); + + TEST_ASSERT_TRUE_MESSAGE(utest_irq_is_flag_set(IRQ_PWR_OFF_REQ), + "a press during normal running must still be delivered to Linux"); + + utest_set_pwrkey_pressed(false); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 100); +} + +// Регрессия (стенд 2026-07-09 17:33): нажатие на ВОЗОБНОВЛЁННОЙ системе жёстко +// рубило питание вместо штатного выключения. Пробуждение из suspend-to-off шло +// через завершение последовательности включения, которое сбрасывало +// linux_booted (правильно для холодного включения, неверно для resume: Linux +// возобновляется, а не грузится) - и первые 20 с после resume короткое нажатие +// попадало в ветку «линукс не загружен» -> hard off. Должно быть: IRQ +// (штатный запрос выключения) без обрубания питания. +static void test_resumed_system_press_is_graceful_not_hard_off(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim_enter_off_mode_sleep(60); + uint32_t boots_before = sim.soc_boot_count; + + // Кнопочное пробуждение (подтверждение +500 мс), отпускание, resume. + utest_mcu_stop_set_button_wake(true); + utest_set_pwrkey_pressed(true); + sim_run_ms(700); + utest_set_pwrkey_pressed(false); + sim_run_ms(3000); // resume завершён, «глотание» снято + TEST_ASSERT_TRUE(sim.soc_boot_count > boots_before); + uint32_t boots_after_resume = sim.soc_boot_count; + + // НОВОЕ нажатие на возобновлённой системе (в пределах бывшего 20-с окна). + utest_set_pwrkey_pressed(true); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 150); + + TEST_ASSERT_TRUE_MESSAGE(utest_irq_is_flag_set(IRQ_PWR_OFF_REQ), + "a press on the resumed system must request a graceful poweroff via IRQ"); + TEST_ASSERT_TRUE_MESSAGE(utest_gpio_get_output_state(gpio_5v) != 0, + "a press on the resumed system must NOT hard-cut the power"); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_after_resume, sim.soc_boot_count, + "a press on the resumed system must not restart the board"); + + utest_set_pwrkey_pressed(false); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 300); + TEST_ASSERT_TRUE_MESSAGE(utest_gpio_get_output_state(gpio_5v) != 0, + "the release must not cut the power either"); +} + +// Легитимный hard-off сохранён: СВЕЖАЯ загрузка (холодное включение), Linux +// ещё не загружен (до 20 с) - короткое нажатие немедленно выключает питание. +static void test_fresh_boot_press_before_linux_up_hard_offs(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim.check_invariants = false; + sim.allow_standby = true; + sim.soc_feeds = true; + sim_boot_to_working(); // WORKING только что; linux_booted=false + // Даём антидребезгу устаканить базовый уровень «отпущено» (из UNINIT): + // нажатие с UNINIT-базы по дизайну не рождает событий («кнопку держали + // при включении»). Остаёмся глубоко внутри 20-с окна !linux_booted. + sim_run_ms(700); + + utest_set_pwrkey_pressed(true); + sim_run_ms(700); // подтверждённое нажатие + utest_set_pwrkey_pressed(false); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 100); // отпускание -> короткое нажатие + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, utest_gpio_get_output_state(gpio_5v), + "a press before Linux is up must hard-off immediately (legit path)"); +} + +// Дедупликация IRQ (стенд 2026-07-09 17:34, двойной "PWR_OFF_REQ latched"): +// флаг взводится по ФРОНТУ нажатия. Если Linux квитирует IRQ, пока кнопку ещё +// держат, ТО ЖЕ нажатие не должно взводить флаг повторно; новое нажатие - +// взводит. +static void test_running_press_single_irq_despite_ack_mid_hold(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim.soc_feeds = true; + sim_boot_to_working(); + sim_run_ms(WBEC_LINUX_BOOT_TIME_MS + 1000); // linux_booted = true + + utest_set_pwrkey_pressed(true); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 100); + TEST_ASSERT_TRUE(utest_irq_is_flag_set(IRQ_PWR_OFF_REQ)); + + // Linux квитировал IRQ, кнопку всё ещё держат. + irq_clear_flags(1u << IRQ_PWR_OFF_REQ); + sim_run_ms(400); + TEST_ASSERT_FALSE_MESSAGE(utest_irq_is_flag_set(IRQ_PWR_OFF_REQ), + "the SAME held press must not re-latch the IRQ after Linux acks it"); + + utest_set_pwrkey_pressed(false); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 100); + + // Новое нажатие - новый фронт - новый IRQ. + utest_set_pwrkey_pressed(true); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 100); + TEST_ASSERT_TRUE_MESSAGE(utest_irq_is_flag_set(IRQ_PWR_OFF_REQ), + "a NEW press must latch a new IRQ"); + utest_set_pwrkey_pressed(false); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 100); +} + +// Регрессия (стенд 2026-07-08): каждое пробуждение из suspend-to-off платило +// фиксированную ~1 с - WAIT_3V3 ждал «самопоявления» 3.3В, которого при +// пробуждении не бывает: 5В не пропадало, PMIC спит и просыпается ТОЛЬКО от +// PWRON. При wake_pending EC обязан жать PWRON сразу (ретраи STEP2/STEP3 +// остаются как fallback). Модель PMIC здесь как на железе: pmic_crashed +// остаётся true и оживает только от удержания PWRON (PMIC_REVIVE_PWRON_MS). +// Бюджет: нажатие ~сразу + оживление 100 мс + подъём 3.3В 100 мс + импульс +// PWROK 100 мс => плата должна стартовать за ~350 мс; без фикса - ~1.3 с. +static void test_stop_wake_presses_pwron_immediately_when_pmic_sleeps(void) +{ + sim_enter_off_mode_sleep(60); + uint32_t boots_before = sim.soc_boot_count; + + // Свежий будильник; PMIC ОСТАЁТСЯ спящим (будится только PWRON) + alarm_fired = true; + sim_run_ms(600); // << 1000 мс старого мёртвого ожидания + + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_before, + "wake with a sleeping PMIC must press PWRON immediately, not after a fixed 1 s wait"); +} + +// Отрицательный случай: касание, которое так и не прошло антидребезг, после +// окна ожидания должно вернуть EC в Stop (а не разбудить плату). +static void test_stop_button_brush_reenters_stop(void) +{ + sim_enter_off_mode_sleep(60); + uint32_t boots_before = sim.soc_boot_count; + + utest_mcu_stop_set_button_wake(true); + sim_run_ms(50); + uint32_t enters_awake = utest_mcu_stop_get_enter_count(); + + // Ждём дольше окна ожидания (debounce + grace) без подтверждённого нажатия. + sim_run_ms(PWRKEY_DEBOUNCE_MS + WBEC_SUSPEND_STOP_BUTTON_GRACE_MS + 200); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_before, sim.soc_boot_count, + "a button brush that never debounces must not wake the board"); + TEST_ASSERT_TRUE_MESSAGE(utest_mcu_stop_get_enter_count() > enters_awake, + "after the grace window the EC must go back to sleep (re-enter Stop)"); + +#if defined EC_GPIO_BUZZER + // Касание без подтверждённого нажатия не даёт обратной связи: бип только + // от загрузки (вход в WORKING холодного старта). + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, buzzer_beep_count, + "a brush must not beep (no wake - no feedback)"); +#endif +} + +// ==================== Два окна подряд: перевооружение кнопки ==================== +// Стенд 2026-07-09: «кнопка молчит во втором окне» (после окна, завершённого +// будильником). Проверяем все порядки: будильник->кнопка (последовательность +// стенда), кнопка->кнопка, кнопка->будильник. Каждое окно - полный цикл: +// объявление, пропадание 3.3В, сон, пробуждение, последовательность включения. + +// Повторное объявление окна после завершившегося resume (система в WORKING). +static void sim_reenter_off_mode_sleep(uint16_t timeout_s) +{ + sim_run_ms(WBEC_LINUX_BOOT_TIME_MS + 1000); // linux_booted после resume + sim_announce_off_mode(timeout_s); + sim_tick(); + sim.pmic_crashed = true; // BL31 снял 3.3В + sim_run_ms(500); +} + +// Пробуждение кнопкой в текущем окне: фронт EXTI + удержание > антидребезга. +static void sim_button_wake(void) +{ + utest_mcu_stop_set_button_wake(true); + utest_set_pwrkey_pressed(true); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 50); + utest_set_pwrkey_pressed(false); + sim.pmic_crashed = false; + sim_run_ms(PWRKEY_DEBOUNCE_MS + 3000); +} + +// Последовательность стенда: окно 1 завершается будильником, в окне 2 - кнопка. +static void test_stop_two_windows_alarm_then_button(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim_enter_off_mode_sleep(60); + + // Окно 1: будильник + alarm_fired = true; + sim.pmic_crashed = false; + sim_run_ms(3000); + uint32_t boots_after_w1 = sim.soc_boot_count; + + // Окно 2: кнопка + sim_reenter_off_mode_sleep(60); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_after_w1, sim.soc_boot_count, + "window 2 must be asleep before the button press"); + sim_button_wake(); + + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_after_w1, + "a button press must wake window 2 after an alarm-ended window 1"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_POWER_KEY, get_poweron_reason_from_regmap(), + "window 2 button wake must report REASON_POWER_KEY"); +} + +static void test_stop_two_windows_button_then_button(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim_enter_off_mode_sleep(60); + sim_button_wake(); // окно 1: кнопка + uint32_t boots_after_w1 = sim.soc_boot_count; + + sim_reenter_off_mode_sleep(60); + sim_button_wake(); // окно 2: снова кнопка + + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_after_w1, + "a button press must wake window 2 after a button-ended window 1"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_POWER_KEY, get_poweron_reason_from_regmap(), + "window 2 button wake must report REASON_POWER_KEY"); +} + +static void test_stop_two_windows_button_then_alarm(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim_enter_off_mode_sleep(60); + sim_button_wake(); // окно 1: кнопка + uint32_t boots_after_w1 = sim.soc_boot_count; + + sim_reenter_off_mode_sleep(60); + alarm_fired = true; // окно 2: будильник + sim.pmic_crashed = false; + sim_run_ms(3000); + + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_after_w1, + "an alarm must wake window 2 after a button-ended window 1"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_RTC_ALARM, get_poweron_reason_from_regmap(), + "window 2 alarm wake must report REASON_RTC_ALARM"); +} + +// Регрессия (стенд 2026-07-09 14:46, главный убийца кнопки): удержание кнопки +// ДОЛЬШЕ grace-окна (>1.2 с). Раньше окно ожидания отсчитывалось от фронта: +// press-begin подтверждался на +500 мс, но короткое нажатие фиксируется только +// на ОТПУСКАНИИ, и на +1200 мс EC уходил обратно в Stop ПОСРЕДИ УДЕРЖАНИЯ - +// нажатие пропадало (4 подряд брошенных нажатия в логе). Прежние тесты держали +// кнопку ровно DEBOUNCE+50 мс - укладывались в старое окно (mock gap). +static void test_stop_button_long_hold_wakes_on_confirm(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim_enter_off_mode_sleep(60); + uint32_t boots_before = sim.soc_boot_count; + + utest_mcu_stop_set_button_wake(true); + utest_set_pwrkey_pressed(true); + sim_run_ms(50); + uint32_t enters_at_hold = utest_mcu_stop_get_enter_count(); + sim_run_ms(2450); // удержание много дольше старого grace (1.2 с) + + // Пробуждение стартует по ПОДТВЕРЖДЕНИЮ (+500 мс), кнопка всё ещё в руке. + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_before, + "a long-held press must wake the board at confirmation, before release"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_POWER_KEY, get_poweron_reason_from_regmap(), + "long-hold button wake must report REASON_POWER_KEY"); + // Ключевое свойство железа: пока кнопку держали до подтверждения, EC не + // уходил обратно в Stop (на железе это теряло нажатие безвозвратно: + // отпускание - растущий фронт, EXTI0 вооружён только на падающий). + TEST_ASSERT_EQUAL_UINT32_MESSAGE(enters_at_hold, utest_mcu_stop_get_enter_count(), + "the EC must NOT re-enter Stop while the button is held"); + + // Отпускание после resume: проглочено, плата продолжает работать. + uint32_t boots_after_wake = sim.soc_boot_count; + utest_set_pwrkey_pressed(false); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 3000); + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_after_wake, sim.soc_boot_count, + "the late release must not restart the board"); + TEST_ASSERT_TRUE_MESSAGE(utest_gpio_get_output_state(gpio_5v) != 0, + "the late release must not power the board off"); +} + +// Семантика ОЧЕНЬ долгого удержания (требование 3): после пробуждения-по- +// подтверждению кнопку продолжают держать до порога длинного нажатия - штатная +// логика длинного нажатия работающей системы берёт верх (принудительное +// выключение). Всё, что короче длинного нажатия, в систему НЕ доставляется. +static void test_stop_button_very_long_hold_reaches_force_off(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim_enter_off_mode_sleep(60); + uint32_t boots_before = sim.soc_boot_count; + + utest_mcu_stop_set_button_wake(true); + utest_set_pwrkey_pressed(true); + sim_run_ms(3000); // пробуждение по подтверждению, держим + TEST_ASSERT_TRUE(sim.soc_boot_count > boots_before); + + // Удержание достигло порога длинного нажатия (8 с; модель антидребезга + // длинное нажатие не эмулирует - защёлку взводим явно). Кнопку отпускаем + // и ДАЁМ АНТИДРЕБЕЗГУ ОТРАБОТАТЬ по сим-времени ДО инъекции: обработчик + // длинного нажатия busy-wait'ит отпускание по антидребезженному уровню, + // а внутри busy-wait сим-время не идёт (иначе тест зависает). + sim.allow_standby = true; + utest_set_pwrkey_pressed(false); + sim_run_ms(PWRKEY_DEBOUNCE_MS + 100); + utest_set_pwrkey_long_press(true); + // Проверяем СРАЗУ после срабатывания: на железе EC уходит в Standby и не + // возвращается; в тестах заглушка mcu_goto_standby возвращается, и дальше + // «загробная жизнь» стаба (V33-loss recovery) расходится с железом. + sim_run_ms(20); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, utest_gpio_get_output_state(gpio_5v), + "a hold reaching the long-press threshold must force the board off"); + TEST_ASSERT_TRUE_MESSAGE(sim.standby_requested, + "force-off must end in the poweroff standby"); +} + +// Регрессия (стенд, хвост нажатия): будильник будит, пока нажатие «в полёте» +// (фронт видели, антидребезг ещё не подтвердил - при пробуждении-по- +// подтверждению это единственная форма «будильник посреди нажатия»). +// Подтверждение и отпускание приходят уже после resume и раньше рождали новое +// «короткое нажатие» - в !linux_booted-ветке это НЕМЕДЛЕННЫЙ hard off + standby +// («poweroff через 11 с после resume»). Порядок 1: отпускание ПОСЛЕ WORKING. +static void test_stop_alarm_wake_mid_hold_release_after_working(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim_enter_off_mode_sleep(60); + uint32_t boots_before = sim.soc_boot_count; + + utest_mcu_stop_set_button_wake(true); + utest_set_pwrkey_pressed(true); + sim_run_ms(200); // нажатие ЕЩЁ НЕ подтверждено (< 500 мс) + alarm_fired = true; // будильник посреди начинающегося нажатия + sim.pmic_crashed = false; + sim_run_ms(3000); // resume завершился, WORKING; кнопку держат + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_before, + "the alarm must wake the board even while a press is in flight"); + uint32_t boots_after_wake = sim.soc_boot_count; + + utest_set_pwrkey_pressed(false); // отпускание уже в рабочем состоянии + sim_run_ms(PWRKEY_DEBOUNCE_MS + 2000); + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_after_wake, sim.soc_boot_count, + "the release of the wake-surviving press must not restart the board"); + TEST_ASSERT_TRUE_MESSAGE(utest_gpio_get_output_state(gpio_5v) != 0, + "the release must NOT power the board off (hard off via !linux_booted)"); + TEST_ASSERT_FALSE_MESSAGE(utest_irq_is_flag_set(IRQ_PWR_OFF_REQ), + "the release must not surface as a power-key event for Linux"); +} + +// Порядок 2: отпускание ДО входа в WORKING (во время последовательности +// включения) - хвост дренируется на завершении включения (:489) либо глотается. +static void test_stop_alarm_wake_mid_hold_release_before_working(void) +{ + utest_pwrkey_enable_debounce_model(true); + sim_enter_off_mode_sleep(60); + uint32_t boots_before = sim.soc_boot_count; + + utest_mcu_stop_set_button_wake(true); + utest_set_pwrkey_pressed(true); + sim_run_ms(200); // нажатие ещё не подтверждено + alarm_fired = true; + sim.pmic_crashed = false; + sim_run_ms(150); // выход состоялся, включение только идёт + utest_set_pwrkey_pressed(false); // отпускание до входа в WORKING + sim_run_ms(PWRKEY_DEBOUNCE_MS + 8000); + + TEST_ASSERT_TRUE_MESSAGE(sim.soc_boot_count > boots_before, + "the alarm wake must complete normally"); + TEST_ASSERT_TRUE_MESSAGE(utest_gpio_get_output_state(gpio_5v) != 0, + "the early release must NOT power the board off"); + TEST_ASSERT_FALSE_MESSAGE(utest_irq_is_flag_set(IRQ_PWR_OFF_REQ), + "the early release must not surface as a power-key event for Linux"); +} + +// Дедлайн-бэкстоп: считается по накопленным тикам RTC WUT (systick в Stop +// заморожен). После timeout_ms / (feed_period*1000) тиков WUT плата будится с +// REASON_WATCHDOG. Регрессия: раньше дедлайн жил на systick, который в Stop +// не идёт. +static void test_stop_deadline_fires_via_wut_ticks(void) +{ + sim_enter_off_mode_sleep(60); // timeout = 60 c + 10 c запас = 70 c + + // Каждый уход в Stop моделирует один истёкший период WUT. + utest_mcu_stop_set_auto_wut_tick(true); + sim_run_ms(500); // >> 70000/4000 = 18 тиков; PMIC не оживляем + + TEST_ASSERT_FALSE_MESSAGE(utest_wbec_get_suspend_started(), + "the WUT-tick deadline must end the suspend window"); + TEST_ASSERT_EQUAL_UINT16_MESSAGE(UTEST_REASON_WATCHDOG, get_poweron_reason_from_regmap(), + "deadline wake must report REASON_WATCHDOG"); + TEST_ASSERT_TRUE_MESSAGE(utest_mcu_stop_get_window_finished(), + "deadline wake must finish the Stop window"); + TEST_ASSERT_TRUE_MESSAGE(utest_rtc_get_periodic_wakeup_disabled(), + "deadline wake must disable the WUT"); + +#if defined EC_GPIO_BUZZER + // Необслуживаемое пробуждение (дедлайн) обязано быть БЕЗЗВУЧНЫМ. + TEST_ASSERT_EQUAL_UINT32_MESSAGE(1, buzzer_beep_count, + "a deadline wake must be silent (only the cold-boot beep is expected)"); +#endif +} + +// Зеркальная регрессия: сам по себе идущий systick (без тиков WUT) НЕ должен +// сработать дедлайн — иначе доказательства, что база времени переехала на WUT, +// нет. Плата обязана спать сколь угодно долго. +static void test_stop_frozen_wut_never_fires_on_systick_alone(void) +{ + sim_enter_off_mode_sleep(60); + uint32_t boots_before = sim.soc_boot_count; + uint32_t enters_before = utest_mcu_stop_get_enter_count(); + + // auto_wut_tick выключен: тиков WUT нет, но сим-время (systick) идёт. + sim_run_ms(120000); // сильно больше timeout (70 c) + + TEST_ASSERT_EQUAL_UINT32_MESSAGE(boots_before, sim.soc_boot_count, + "a frozen WUT accumulator must never fire the deadline on systick alone"); + TEST_ASSERT_TRUE_MESSAGE(utest_wbec_get_suspend_started(), + "board must still be asleep in the Stop window"); + TEST_ASSERT_TRUE_MESSAGE(utest_mcu_stop_get_enter_count() > enters_before, + "the EC must keep sleeping in Stop across the window"); +} + +// Инвариант IWDG (прямая регрессия на причину отказа Standby): аппаратный +// watchdog кормится не реже одного раза на каждый уход в Stop (E1, перед сном), +// поэтому за окно сна он никогда не досчитает до ~10 с. +static void test_stop_feeds_iwdg_on_every_feed_tick(void) +{ + sim_enter_off_mode_sleep(60); + + uint32_t reloads_before = utest_watchdog_get_reload_count(); + uint32_t enters_before = utest_mcu_stop_get_enter_count(); + sim_run_ms(1000); + uint32_t reloads = utest_watchdog_get_reload_count() - reloads_before; + uint32_t enters = utest_mcu_stop_get_enter_count() - enters_before; + + TEST_ASSERT_TRUE_MESSAGE(enters > 0, "EC must keep entering Stop"); + TEST_ASSERT_TRUE_MESSAGE(reloads >= enters, + "the IWDG must be fed at least once per Stop feed-tick"); +} + int main(void) { UNITY_BEGIN(); @@ -720,6 +1446,29 @@ int main(void) 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); + RUN_TEST(test_suspend_off_second_window_no_stale_alarm_at_entry); + RUN_TEST(test_suspend_off_alarm_fired_before_sleep_start_wakes_at_entry); + + // Stop 1: окно сна suspend-to-off + RUN_TEST(test_stop_window_arms_safe_and_wakes_on_fresh_alarm); + RUN_TEST(test_stop_wake_by_button_edge_then_debounced_press); + RUN_TEST(test_stop_button_wake_press_not_redelivered_to_linux); + RUN_TEST(test_running_press_still_delivers_pwr_off_req); + RUN_TEST(test_resumed_system_press_is_graceful_not_hard_off); + RUN_TEST(test_fresh_boot_press_before_linux_up_hard_offs); + RUN_TEST(test_running_press_single_irq_despite_ack_mid_hold); + RUN_TEST(test_stop_wake_presses_pwron_immediately_when_pmic_sleeps); + RUN_TEST(test_stop_button_brush_reenters_stop); + RUN_TEST(test_stop_two_windows_alarm_then_button); + RUN_TEST(test_stop_two_windows_button_then_button); + RUN_TEST(test_stop_two_windows_button_then_alarm); + RUN_TEST(test_stop_button_long_hold_wakes_on_confirm); + RUN_TEST(test_stop_button_very_long_hold_reaches_force_off); + RUN_TEST(test_stop_alarm_wake_mid_hold_release_after_working); + RUN_TEST(test_stop_alarm_wake_mid_hold_release_before_working); + RUN_TEST(test_stop_deadline_fires_via_wut_ticks); + RUN_TEST(test_stop_frozen_wut_never_fires_on_systick_alone); + RUN_TEST(test_stop_feeds_iwdg_on_every_feed_tick); #if defined(WBEC_HAS_WARM_RESET) RUN_TEST(test_escalation_alternates_warm_and_hard); #endif diff --git a/unittests/wbec/wbec_test_stubs.c b/unittests/wbec/wbec_test_stubs.c index 889d23b..8d81196 100644 --- a/unittests/wbec/wbec_test_stubs.c +++ b/unittests/wbec/wbec_test_stubs.c @@ -87,6 +87,8 @@ void linux_cpu_pwr_seq_off_and_goto_standby(uint16_t wakeup_after_s) } } +void linux_cpu_pwr_seq_wake_beep_request(void) {} + void linux_cpu_pwr_seq_on(void) { linux_pwr_state.pwr_on_called = true; @@ -151,6 +153,10 @@ int16_t temperature_control_get_temperature_c_x100(void) return temp_ctrl_state.temperature_c_x100; } +// suspend-to-off: перевод управляемых выходов в безопасное состояние на окно сна +void temperature_control_suspend(bool on) { (void)on; } +void gpio_suspend(bool on) { (void)on; } + static bool alarm_enabled = false; void utest_rtc_alarm_reset(void)