diff --git a/CMakePresets.json b/CMakePresets.json index 294d1c6..b62a268 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -142,6 +142,7 @@ "test_bsp_led", "test_log", "test_bsp_opto", + "test_bsp_button", "test_bsp_can", "uart_host_mock_example", "test_ring_buffer", @@ -156,9 +157,10 @@ "targets": [ "test_bsp_led", "test_log", + "test_bsp_opto", + "test_bsp_button", "test_bsp_can", "uart_host_mock_example", - "test_bsp_opto", "test_ring_buffer", "test_timeout_pattern", "uart_host_mock_example" @@ -170,6 +172,7 @@ "configurePreset": "target-debug", "targets": [ "test_host_uart", + "test_hil_button", "test_hil_can", "test_hil_opto" ] diff --git a/bsp/CMakeLists.txt b/bsp/CMakeLists.txt index 65f2e0b..ea7ddf8 100644 --- a/bsp/CMakeLists.txt +++ b/bsp/CMakeLists.txt @@ -14,6 +14,8 @@ add_subdirectory(tick) add_subdirectory(uart_host) add_subdirectory(opto) add_subdirectory(can) +add_subdirectory(button) + # Подавляем предупреждения при компиляции собственных .c файлов библиотеки target_compile_options(bsp_board PRIVATE -w) diff --git a/bsp/button/CMakeLists.txt b/bsp/button/CMakeLists.txt new file mode 100644 index 0000000..f048cb1 --- /dev/null +++ b/bsp/button/CMakeLists.txt @@ -0,0 +1,15 @@ +if(BUILD_TESTS_HOST) + return() +endif() + +add_library(bsp_button STATIC src/button.c) + +target_include_directories( + bsp_button + PUBLIC include/ + PRIVATE src/) + +target_link_libraries( + bsp_button + PUBLIC bsp_status + PRIVATE bsp_board sdk_gpio) diff --git a/bsp/button/README.md b/bsp/button/README.md new file mode 100644 index 0000000..0f8ee89 --- /dev/null +++ b/bsp/button/README.md @@ -0,0 +1,224 @@ +# bsp_button — тактовые кнопки + +Чтение двух тактовых кнопок SWT6x6 с программным debounce. +Предоставляет мгновенное сырое чтение, стабильное состояние и одноразовые +события нажатия/отпускания. + +--- + +## Аппаратура + +| Кнопка | Пин MCU | GPIO | Схема | Нажатие | +|----------------|--------------|-----------|--------------------------------|---------| +| `BSP_BUTTON_1` | GPIO_B1_14 | GPIO2[30] | SWT6x6, pull-up к 3V3 внешний | LOW | +| `BSP_BUTTON_2` | GPIO_B1_15 | GPIO2[31] | SWT6x6, pull-up к 3V3 внешний | LOW | + +Пины настроены в `BOARD_InitPins()` (`generated/pin_mux.c`) как INPUT с включённым +гистерезисом, без внутренней подтяжки (`0x0100B0`). `bsp_button_init()` не трогает +GPIO — только сбрасывает внутреннее состояние модуля. + +--- + +## Принцип работы + +```bash +GPIO2[30] / GPIO2[31] + ↓ + GPIO_ReadPinInput() ← вызывается в bsp_button_poll() + ↓ + debounce (счётчик) ← 4 одинаковых сэмпла подряд = 20 мс + ↓ + stable state + события ← evt_pressed / evt_released + ↓ + bsp_button_is_pressed() + bsp_button_get_event_pressed() + bsp_button_get_event_released() +``` + +- **Polling** — `bsp_button_poll()` вызывается снаружи каждые 5 мс. Без прерываний. +- **Debounce** — счётчик подтверждений: 4 одинаковых сэмпла подряд фиксируют + переход. Глитч (смена уровня до набора порога) сбрасывает счётчик. +- **События** — одноразовые флаги `evt_pressed` / `evt_released`, сбрасываются + при первом обращении через `get_event_*()`. + +Максимальная задержка реакции = период поллинга = 5 мс. Для навигации по меню +и производственного теста этого достаточно — порог восприятия задержки UI +около 50–100 мс. + +--- + +## Быстрый старт + +```c +#include "bsp/button.h" + +/* После board_hw_init(): */ +bsp_button_init(); + +/* bare-metal — вызывать каждые 5 мс из tick-коллбэка: */ +bsp_button_poll(); + +/* В основном цикле: */ +if (bsp_button_get_event_pressed(BSP_BUTTON_1)) { + /* однократное срабатывание по нажатию */ +} + +if (bsp_button_is_pressed(BSP_BUTTON_2)) { + /* кнопка удерживается */ +} + +/* FreeRTOS — в таске: */ +vTaskDelay(pdMS_TO_TICKS(5)); +bsp_button_poll(); +if (bsp_button_get_event_pressed(BSP_BUTTON_1)) { + xQueueSend(btn_queue, &btn_event, 0); +} +``` + +### Startup check (bootloader) + +```c +/* Сырое чтение без debounce — сразу после board_hw_init(), + до вызова bsp_button_init(). */ +if (bsp_button_read(BSP_BUTTON_1)) { + /* кнопка удерживается при старте → войти в режим обновления */ +} +``` + +--- + +## API + +### `bsp_button_init()` + +Сбрасывает внутреннее состояние (счётчики debounce, флаги событий). +GPIO уже настроен в `BOARD_InitPins()` — вызывать после `board_hw_init()`. + +### `bsp_button_read(btn)` + +Мгновенное сырое чтение пина без debounce. Возвращает `true` если кнопка +нажата прямо сейчас. Предназначено для проверки при старте (bootloader hold-check). + +### `bsp_button_poll()` + +Один шаг debounce. Вызывать строго каждые 5 мс — из tick-коллбэка +(bare-metal) или таска (FreeRTOS). Обновляет стабильное состояние +и выставляет одноразовые события для обеих кнопок за один вызов. + +### `bsp_button_is_pressed(btn)` + +Стабильное состояние после debounce. `true` = кнопка удерживается нажатой. +Не сбрасывается при чтении. + +### `bsp_button_get_event_pressed(btn)` + +Одноразовое событие нажатия. Возвращает `true` один раз после того как +debounce зафиксировал переход в нажатое состояние. **Флаг сбрасывается при вызове.** + +### `bsp_button_get_event_released(btn)` + +Одноразовое событие отпускания. Возвращает `true` один раз после того как +debounce зафиксировал переход в отпущенное состояние. **Флаг сбрасывается при вызове.** + +--- + +## Логика длинного/короткого нажатия + +`bsp_button` намеренно не реализует логику длинного/короткого нажатия — +это интерпретация событий, зависящая от контекста приложения. + +В `tft_app` реализуется отдельным модулем `button_handler` поверх событий BSP: + +```bash +bsp_button → button_handler → app (меню, навигация) +``` + +`button_handler` хранит `press_start_ms`, использует `bsp_tick_get_ms()` +и вызывает коллбэк с типом действия (`SHORT_PRESS`, `LONG_PRESS`, `REPEAT`). +Не зависит от железа — тестируется на хосте как категория A (без fff). + +--- + +## Конфигурация debounce + +```c +/* bsp/button/src/button.c */ +#define BUTTON_DEBOUNCE_SAMPLES 4U /* 4 × 5 мс = 20 мс */ +``` + +При изменении периода поллинга нужно пересчитать `BUTTON_DEBOUNCE_SAMPLES` +чтобы сохранить целевое время debounce (рекомендуется 15–30 мс). + +--- + +## Тестирование + +### Host unit-тесты + +Категория **B** — `button.c` вызывает `GPIO_ReadPinInput()` из `fsl_gpio.h`. +SDK-функция мокируется через fff в тестовом файле. +Stub `fsl_gpio.h` уже существует в `tests/host/mocks/`. + +```cmake +add_host_test( + NAME test_bsp_button + SOURCES button/test_bsp_button.c + ${CMAKE_SOURCE_DIR}/bsp/button/src/button.c + INCLUDES + ${CMAKE_SOURCE_DIR}/bsp/button/include + ${CMAKE_SOURCE_DIR}/bsp/common/include + MOCKS + ${BSP_MOCKS_DIR} +) +``` + +Покрытие: инициализация, сырое чтение, debounce нажатия/отпускания, +потребление событий, сброс счётчика при глитче, независимость кнопок, +граничные значения индекса. + +### HIL-тест (интерактивный) + +Кнопки расположены на плате таргета — оператор нажимает вручную по подсказкам. + +```bash +just host::hil-button # запускать отдельно, не входит в hil-run +``` + +C-прошивка: `tests/target/hil_button/` — CLI через `bsp_uart_host`. +pytest: `tools/hil/04_test_button.py` — помечен `@pytest.mark.interactive`. + +Команды CLI прошивки: + +| Команда | Ответ | Описание | +|--------------|-------------|--------------------------------------------| +| `PING` | `PONG` | Проверка канала | +| `READ ` | `1` / `0` | Сырое состояние (без debounce) | +| `STATE `| `1` / `0` | Стабильное состояние после debounce | +| `EVENT_P `| `1` / `0` | `get_event_pressed`, сбрасывает флаг | +| `EVENT_R `| `1` / `0` | `get_event_released`, сбрасывает флаг | + +--- + +## Зависимости + +| Зависимость | Тип | Описание | +|--------------|---------|----------------------------------------------| +| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API | +| `bsp_board` | PRIVATE | Транзитивно: `pin_mux.h`, clock, SDK headers | +| `sdk_gpio` | PRIVATE | `fsl_gpio.h` — `GPIO_ReadPinInput()` | + +--- + +## Подключение + +```cmake +# bsp/CMakeLists.txt +add_subdirectory(button) + +# firmware/test/CMakeLists.txt или firmware/tft_app/CMakeLists.txt +target_link_libraries(firmware_test PRIVATE + bsp_board + bsp_tick + bsp_button +) +``` diff --git a/bsp/button/include/bsp/button.h b/bsp/button/include/bsp/button.h new file mode 100644 index 0000000..9412761 --- /dev/null +++ b/bsp/button/include/bsp/button.h @@ -0,0 +1,88 @@ +/* + * bsp_button — тактовые кнопки TactBut1 / TactBut2 + * + * Аппаратура: + * TactBut1 — GPIO2 pin 30 (GPIO_B1_14), подтяжка к 3V3 внешняя, нажатие = LOW + * TactBut2 — GPIO2 pin 31 (GPIO_B1_15), подтяжка к 3V3 внешняя, нажатие = LOW + * + * Пины инициализированы в BOARD_InitPins() (generated/pin_mux.c). + * bsp_button_init() не трогает GPIO — только сбрасывает внутреннее состояние. + * + * Использование (bare-metal): + * bsp_button_init(); + * // в tick-коллбэке каждые 5 мс: + * bsp_button_poll(); + * // в основном цикле: + * if (bsp_button_get_event_pressed(BSP_BUTTON_1)) { ... } + * + * Использование (FreeRTOS): + * // в таске с vTaskDelay(5): + * bsp_button_poll(); + * if (bsp_button_get_event_pressed(BSP_BUTTON_1)) { xQueueSend(...); } + */ + +#ifndef BSP_BUTTON_H +#define BSP_BUTTON_H + +#include "bsp/status.h" + +#include +#include + +/* ------------------------------------------------------------------------- + * Типы + * ---------------------------------------------------------------------- */ + +typedef enum +{ + BSP_BUTTON_1 = 0, /* TactBut1 — GPIO2/30 */ + BSP_BUTTON_2 = 1, /* TactBut2 — GPIO2/31 */ + BSP_BUTTON_COUNT, +} bsp_button_t; + +/* ------------------------------------------------------------------------- + * API + * ---------------------------------------------------------------------- */ + +/** + * Инициализация модуля. + * Сбрасывает внутреннее состояние debounce. + * GPIO уже настроен в BOARD_InitPins() — вызывать после board_hw_init(). + */ +bsp_status_t bsp_button_init(void); + +/** + * Мгновенное сырое чтение пина без debounce. + * Предназначено для проверки при старте (например, bootloader hold-check). + * Возвращает true если кнопка нажата прямо сейчас. + */ +bool bsp_button_read(bsp_button_t btn); + +/** + * Шаг debounce — вызывать строго каждые 5 мс. + * Обновляет стабильное состояние и выставляет одноразовые события. + * Безопасно вызывать из ISR (tick) или из таска FreeRTOS. + */ +void bsp_button_poll(void); + +/** + * Возвращает стабильное состояние после debounce. + * true = кнопка удерживается нажатой. + */ +bool bsp_button_is_pressed(bsp_button_t btn); + +/** + * Одноразовое событие нажатия. + * Возвращает true один раз после того как debounce зафиксировал переход + * в нажатое состояние. Флаг сбрасывается при вызове. + */ +bool bsp_button_get_event_pressed(bsp_button_t btn); + +/** + * Одноразовое событие отпускания. + * Возвращает true один раз после того как debounce зафиксировал переход + * в отпущенное состояние. Флаг сбрасывается при вызове. + */ +bool bsp_button_get_event_released(bsp_button_t btn); + +#endif /* BSP_BUTTON_H */ \ No newline at end of file diff --git a/bsp/button/src/button.c b/bsp/button/src/button.c new file mode 100644 index 0000000..6646456 --- /dev/null +++ b/bsp/button/src/button.c @@ -0,0 +1,146 @@ +#include "bsp/button.h" + +#include "bsp/status.h" +#include "fsl_gpio.h" +#include "pin_mux.h" + +#include +#include +/* ------------------------------------------------------------------------- + * Конфигурация debounce + * ---------------------------------------------------------------------- */ + +/** Количество последовательных одинаковых сэмплов для фиксации состояния. + * При poll-периоде 5 мс: 4 × 5 мс = 20 мс. */ +#define BUTTON_DEBOUNCE_SAMPLES 4U + +/* ------------------------------------------------------------------------- + * Внутренние типы + * ---------------------------------------------------------------------- */ + +typedef struct +{ + bool stable; /* текущее стабильное (после debounce) состояние */ + bool last_raw; /* последнее сырое значение для детектирования смены */ + uint8_t count; /* счётчик подтверждений текущего raw-значения */ + bool evt_pressed; /* одноразовый флаг: зафиксировано нажатие */ + bool evt_released; /* одноразовый флаг: зафиксировано отпускание */ +} button_state_t; + +/* ------------------------------------------------------------------------- + * Таблица пинов (индексируется по bsp_button_t) + * ---------------------------------------------------------------------- */ + +typedef struct +{ + GPIO_Type *gpio; + uint32_t pin; +} button_pin_t; + +static const button_pin_t K_PINS[BSP_BUTTON_COUNT] = { + [BSP_BUTTON_1] = { BOARD_INITPINS_TactBut1_GPIO, BOARD_INITPINS_TactBut1_GPIO_PIN }, + [BSP_BUTTON_2] = { BOARD_INITPINS_TactBut2_GPIO, BOARD_INITPINS_TactBut2_GPIO_PIN }, +}; + +/* ------------------------------------------------------------------------- + * Состояние модуля + * ---------------------------------------------------------------------- */ + +static button_state_t g_s_state[BSP_BUTTON_COUNT]; +static bool g_s_initialized = false; + +/* ------------------------------------------------------------------------- + * Реализация + * ---------------------------------------------------------------------- */ + +bsp_status_t bsp_button_init(void) +{ + memset(g_s_state, 0, sizeof(g_s_state)); + g_s_initialized = true; + return BSP_OK; +} + +bool bsp_button_read(bsp_button_t btn) +{ + if ((uint32_t) btn >= (uint32_t) BSP_BUTTON_COUNT) + { + return false; + } + /* LOW = нажата (кнопка замыкает на GND, pull к 3V3 внешний) */ + return (GPIO_ReadPinInput(K_PINS[btn].gpio, K_PINS[btn].pin) == 0U); +} + +void bsp_button_poll(void) +{ + if (!g_s_initialized) + { + return; + } + + for (uint32_t i = 0U; i < (uint32_t) BSP_BUTTON_COUNT; i++) + { + button_state_t *button_state = &g_s_state[i]; + + bool raw = (GPIO_ReadPinInput(K_PINS[i].gpio, K_PINS[i].pin) == 0U); + + if (raw == button_state->last_raw) + { + /* Сигнал стабилен — накапливаем подтверждения */ + if (button_state->count < BUTTON_DEBOUNCE_SAMPLES) + { + button_state->count++; + } + + if (button_state->count == BUTTON_DEBOUNCE_SAMPLES && raw != button_state->stable) + { + /* Порог достигнут, состояние изменилось */ + button_state->stable = raw; + if (raw) + { + button_state->evt_pressed = true; + } + else + { + button_state->evt_released = true; + } + } + } + else + { + /* Смена уровня — сбрасываем счётчик */ + button_state->count = 1U; + button_state->last_raw = raw; + } + } +} + +bool bsp_button_is_pressed(bsp_button_t btn) +{ + if ((uint32_t) btn >= (uint32_t) BSP_BUTTON_COUNT) + { + return false; + } + return g_s_state[btn].stable; +} + +bool bsp_button_get_event_pressed(bsp_button_t btn) +{ + if ((uint32_t) btn >= (uint32_t) BSP_BUTTON_COUNT) + { + return false; + } + bool evt = g_s_state[btn].evt_pressed; + g_s_state[btn].evt_pressed = false; + return evt; +} + +bool bsp_button_get_event_released(bsp_button_t btn) +{ + if ((uint32_t) btn >= (uint32_t) BSP_BUTTON_COUNT) + { + return false; + } + bool evt = g_s_state[btn].evt_released; + g_s_state[btn].evt_released = false; + return evt; +} \ No newline at end of file diff --git a/docs/testing/hil/HIL_BENCH.md b/docs/testing/hil/HIL_BENCH.md index a1b3916..f8615ec 100644 --- a/docs/testing/hil/HIL_BENCH.md +++ b/docs/testing/hil/HIL_BENCH.md @@ -39,6 +39,7 @@ M5: реле AW9523B через ULN2003A, нормально разомкнут | RLY4 | EXT_IN2 | `BSP_OPTO_CH_IN2` | GPIO_AD_B1_05 | GPIO1[21] | **Логика сигнала:** + - Реле разомкнуто → нет тока через оптопару → пин LOW → `BSP_OPTO_STATE_INACTIVE` - Реле замкнуто → ток через оптопару → пин HIGH → `BSP_OPTO_STATE_ACTIVE` @@ -87,14 +88,19 @@ M5 → хост: {"ok": true, "opto_ch": 1, "relay": 3, "state": true}\r\n **Доступные команды агента:** -| Команда | Параметры | Действие | -|----------------|------------------------|---------------------------------------| -| `ping` | — | Проверка связи | -| `power` | `state: bool` | RLY1 — питание таргета | -| `relay_set` | `ch: 1-4, state: bool` | Прямое управление реле | -| `relay_all_off`| — | Выключить все реле | -| `opto_set` | `ch: 1-3, state: bool` | Управление оптоканалом таргета (через маппинг) | -| `opto_all_off` | — | Выключить все оптоканалы | +| Команда | Параметры | Действие | +|----------------|------------------------------------|---------------------------------------------------| +| `ping` | — | Проверка связи | +| `info` | — | Версия, состояние CAN и AW9523, статус реле | +| `power` | `state: bool` | RLY1 — питание таргета | +| `relay_set` | `ch: 1-4, state: bool` | Прямое управление реле | +| `relay_get` | `ch: 1-4` | Прочитать текущее состояние реле | +| `relay_all_off`| — | Выключить все реле | +| `opto_set` | `ch: 1-3, state: bool` | Управление оптоканалом таргета (через маппинг) | +| `opto_all_off` | — | Выключить все оптоканалы | +| `input_read` | `ch: 1-8` | Прочитать вход стенда SYS_IN (оптопара на M5) | +| `can_send` | `id: int, data: list[int], ext: bool=false` | Отправить CAN-фрейм с шины M5 | +| `can_recv` | `timeout_ms: int=500` | Принять CAN-фрейм на M5 (ошибка при таймауте) | **Деплой агента на M5:** @@ -131,7 +137,7 @@ pytest → pyserial → MCU-Link VCOM → LPUART1 → прошивка тарг `tools/hil/conftest.py` содержит всю логику подготовки стенда: -``` +```bash фикстура m5 (scope=module) ├── подключиться к M5 агенту ├── agent.power(True) ← RLY1: питание таргета ON @@ -176,6 +182,22 @@ just host::m5-power off just host::uart-monitor ``` +### HIL-тесты + +```bash +# Запустить все автоматические тесты (без оператора) +just host::hil-run + +# Запустить все интерактивные тесты (требуют оператора) +just host::hil-run-interactive + +# Конкретные тесты +just host::hil-uart +just host::hil-opto +just host::hil-can +just host::hil-button # интерактивный — нажатие кнопок оператором +``` + --- ## Добавление нового сигнала стенда diff --git a/docs/testing/hil/HIL_CREATE_TEST.md b/docs/testing/hil/HIL_CREATE_TEST.md index 0355055..87aa1c1 100644 --- a/docs/testing/hil/HIL_CREATE_TEST.md +++ b/docs/testing/hil/HIL_CREATE_TEST.md @@ -17,12 +17,15 @@ just/build.just build-hil ``` -Два типа тестов: +Три типа тестов: -| Тип | Использует M5 | Когда применять | -|-----|--------------|-----------------| -| **Базовый** | Нет | Тестирование UART CLI, алгоритмов, таймингов | -| **С M5** | Да | Тестирование GPIO, оптовходов, реле, питания | +| Тип | Использует M5 | Запуск | Когда применять | +|-----|--------------|--------|-----------------| +| **Базовый** | Нет | `hil-run` | Тестирование UART CLI, алгоритмов, таймингов | +| **С M5** | Да | `hil-run` | Тестирование GPIO, оптовходов, реле, питания | +| **Интерактивный** | Нет / Да | `hil-run-interactive` | Периферия требует действий оператора: кнопки, дисплей | + +Интерактивные тесты помечаются `@pytest.mark.interactive` и **никогда не входят в `hil-run`** — они требуют живого оператора и не пригодны для CI. --- @@ -286,6 +289,54 @@ def wait_until(ser, cmd, expected, timeout_s=1.0): raise TimeoutError(f"Ожидали {expected!r} от '{cmd}'") ``` +### Интерактивный тест (оператор нажимает кнопки / смотрит на дисплей) + +Добавить маркер на класс. Для ввода использовать `/dev/tty` напрямую — `input()` не работает под захватом pytest даже с `-s`: + +```python +"""test_.py — интерактивный HIL-тест <что тестируем>.""" +import time +import pytest +from conftest import uart_cmd + +SETTLE_S = 0.10 # ждать после действия оператора (debounce и т.п.) + + +def _operator_prompt(msg: str) -> None: + """Вывести подсказку и дождаться Enter от оператора. + Читает /dev/tty напрямую — работает независимо от захвата pytest.""" + with open("/dev/tty", "w") as tty_out: + tty_out.write(f"\n >>> {msg}\n Нажмите Enter когда готово...\n") + tty_out.flush() + with open("/dev/tty", "r") as tty_in: + tty_in.readline() + time.sleep(SETTLE_S) + + +@pytest.mark.interactive +class Test: + + @pytest.fixture(autouse=True) + def _setup(self, uart_): + self.ser = uart_ + + def test_ping(self): + assert uart_cmd(self.ser, "PING") == "PONG" + + def test_something(self): + _operator_prompt("Выполни действие X на плате") + assert uart_cmd(self.ser, "MY_CMD") == "EXPECTED" +``` + +**Запуск интерактивных тестов:** + +```bash +just host::hil-run-interactive # все интерактивные +just host::hil-button # конкретный интерактивный +``` + +**Правило:** интерактивные тесты **не добавлять** в `hil-run` — они входят только в `hil-run-interactive`. + --- ## Шаг 7 — `just/host.just`: добавить рецепт (опционально) @@ -298,6 +349,16 @@ hil-: uv run --directory {{ HIL_DIR }} pytest test_.py -v ``` +Для интерактивных — добавить флаг `-s`: + +```just +[doc('Запустить HIL-тест (интерактивный, требует оператора)')] +[group('hil')] +hil-: + HIL_BUILD_DIR={{ _hil_build }} \ + uv run --directory {{ HIL_DIR }} pytest test_.py -s -v +``` + --- ## Полный цикл @@ -361,14 +422,25 @@ uart.close() test_something ## Чеклист +### Автоматический тест (базовый или с M5) + ```bash -[ ] tests/target//main.c — C-прошивка с CLI + READY-паттерн -[ ] tests/target//CMakeLists.txt — сборка с bsp_boot_ram -[ ] tests/target/CMakeLists.txt — add_subdirectory() -[ ] CMakePresets.json — добавить test_ в targets -[ ] tools/hil/conftest.py — loaded_ + uart_ -[ ] tools/hil/test_.py — pytest-тесты -[ ] just/host.just — рецепт hil- (опционально) +[ ] tests/target//main.c — C-прошивка с CLI + READY-паттерн +[ ] tests/target//CMakeLists.txt — сборка с bsp_boot_ram +[ ] tests/target/CMakeLists.txt — add_subdirectory() +[ ] CMakePresets.json — добавить test_ в targets +[ ] tools/hil/conftest.py — loaded_ + uart_ +[ ] tools/hil/test_.py — pytest-тесты +[ ] just/host.just — рецепт hil- (опционально) [ ] just build::build-hil — зелёная сборка -[ ] just host::hil- — зелёный прогон +[ ] just host::hil- — зелёный прогон +``` + +### Интерактивный тест (дополнительно к базовому чеклисту) + +```bash +[ ] @pytest.mark.interactive — пометить класс в test_.py +[ ] just/host.just — рецепт hil- с флагом -s +[ ] just host::hil-run-interactive — зелёный прогон +[ ] убедиться что just host::hil-run — NOT в выборке (маркер исключает) ``` diff --git a/docs/testing/hil/HIL_HOW_TO.md b/docs/testing/hil/HIL_HOW_TO.md index 07c661f..5eb657f 100644 --- a/docs/testing/hil/HIL_HOW_TO.md +++ b/docs/testing/hil/HIL_HOW_TO.md @@ -110,20 +110,31 @@ ls build/target-debug/tests/target/ ## Шаг 4 — Запуск тестов (на хосте, вне контейнера) -### Все HIL-тесты +### Автоматические HIL-тесты (без оператора) ```bash just host::hil-run ``` -pytest обходит все `test_*.py` в `tools/hil/`, запускает их по очереди. +pytest обходит все `test_*.py` в `tools/hil/`, **исключая** помеченные `@pytest.mark.interactive`. Каждый файл — своя загрузка ELF, свой UART-сеанс, MCU перезагружается между файлами. -### Конкретный тест +### Интерактивные HIL-тесты (требуют оператора) + +```bash +just host::hil-run-interactive # все интерактивные (кнопки, дисплей и т.п.) +just host::hil-button # конкретный интерактивный +``` + +Запускаются с флагом `-s` — pytest не перехватывает stdin/stdout, оператор видит +подсказки и может нажимать Enter. Не входят в `hil-run` и не запускаются в CI. + +### Конкретный автоматический тест ```bash just host::hil-uart # только test_uart.py just host::hil-opto # только test_opto.py +just host::hil-can # только test_can.py ``` ### Один тест-кейс (для отладки) @@ -141,8 +152,6 @@ uv run --directory tools/hil pytest test_opto.py -v --no-load Удобно при отладке тестов когда прошивка уже в RAM и не нужно каждый раз ждать загрузки через pyOCD. ---- - ## Шаг 5 — Интерпретация результатов ### Зелёный прогон diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index f84af92..517d32c 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -19,6 +19,7 @@ target_link_libraries( bsp_led bsp_tick bsp_can + bsp_button bsp_uart_host bsp_boot_xip bsp_opto diff --git a/firmware/test/main.c b/firmware/test/main.c index 9dda0c7..c284128 100644 --- a/firmware/test/main.c +++ b/firmware/test/main.c @@ -1,5 +1,6 @@ #include "board.h" +#include "bsp/button.h" #include "bsp/can.h" #include "bsp/led.h" #include "bsp/opto.h" @@ -45,10 +46,11 @@ static void on_opto_change(bsp_opto_ch_t ch, bsp_opto_state_t state) int main(void) { - const uint16_t DELAY_MS = 10; + const uint16_t DELAY_MS = 5; const uint32_t UART_BAUDRATE = 115200; const uint32_t CAN_BAUDRATE = 125000; board_hw_init(); + bsp_button_init(); bsp_led_init(); bsp_tick_init(); bsp_uart_host_init(UART_BAUDRATE); @@ -88,6 +90,27 @@ int main(void) while (1) { bsp_opto_process(); + bsp_button_poll(); + + if (bsp_button_get_event_pressed(BSP_BUTTON_1)) + { + LOG_D("BUTTON", "B1 was pressed"); + } + + if (bsp_button_get_event_released(BSP_BUTTON_1)) + { + LOG_D("BUTTON", "B1 was released"); + } + + if (bsp_button_get_event_pressed(BSP_BUTTON_2)) + { + LOG_D("BUTTON", "B2 was pressed"); + } + + if (bsp_button_get_event_released(BSP_BUTTON_2)) + { + LOG_D("BUTTON", "B2 was released"); + } if (bsp_can_receive(&rx_frame, 0) == BSP_OK) { diff --git a/just/host.just b/just/host.just index f137a7a..4703511 100644 --- a/just/host.just +++ b/just/host.just @@ -330,11 +330,17 @@ flash-swd-erase: _hil_build := BUILD_DIR / "target-debug" -[doc('Запустить все HIL-тесты')] +[doc('Запустить все не интерактивные HIL-тесты')] [group('hil')] hil-run: HIL_BUILD_DIR={{ _hil_build }} \ - uv run --directory {{ HIL_DIR }} pytest -v + uv run --directory {{ HIL_DIR }} pytest -m "not interactive" -v + +[doc('Запустить все интерактивные HIL-тесты: кнопки, дисплей')] +[group('hil')] +hil-run-interactive: + HIL_BUILD_DIR={{ _hil_build }} \ + uv run --directory {{ HIL_DIR }} pytest -m interactive -s -v [doc('Запустить HIL-тест UART')] [group('hil')] @@ -348,6 +354,12 @@ hil-opto: HIL_BUILD_DIR={{ _hil_build }} \ uv run --directory {{ HIL_DIR }} pytest 02_test_opto.py -v +[doc('Запустить HIL-тест кнопок')] +[group('hil')] +hil-button: + HIL_BUILD_DIR={{ _hil_build }} \ + uv run --directory {{ HIL_DIR }} pytest 04_test_button.py -s -v + [doc('Запустить HIL тест bsp_can')] [group('hil')] hil-can: diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index dd10d9d..800c755 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -143,3 +143,18 @@ add_host_test( ${CMAKE_SOURCE_DIR}/utils/ MOCKS ${BSP_MOCKS_DIR}) + +# ----------------------------------------------------------------------------- +# Тест логики bsp_button (категория B — SDK мокается через fff в тест-файле) +# ----------------------------------------------------------------------------- +add_host_test( + NAME + test_bsp_button + SOURCES + button/test_bsp_button.c + ${CMAKE_SOURCE_DIR}/bsp/button/src/button.c + INCLUDES + ${CMAKE_SOURCE_DIR}/bsp/button/include + ${CMAKE_SOURCE_DIR}/bsp/common/include + MOCKS + ${BSP_MOCKS_DIR}) diff --git a/tests/host/button/test_bsp_button.c b/tests/host/button/test_bsp_button.c new file mode 100644 index 0000000..466cff1 --- /dev/null +++ b/tests/host/button/test_bsp_button.c @@ -0,0 +1,295 @@ +/* + * test_bsp_button.c — host unit-тесты для bsp_button + * + * Категория B: button.c использует fsl_gpio.h. + * GPIO_ReadPinInput мокируется через fff. + * GPIO_PinInit не вызывается (bsp_button_init не трогает GPIO) — фейк не нужен. + */ + +#include "fff.h" +#include "unity.h" + +DEFINE_FFF_GLOBALS; /* ровно один раз на весь .c файл */ + +#include "fsl_gpio.h" /* stub из tests/host/mocks/ */ + +/* Мок единственной SDK-функции, которую вызывает button.c */ +FAKE_VALUE_FUNC(uint32_t, GPIO_ReadPinInput, GPIO_Type *, uint32_t); + +#include "bsp/button.h" /* тестируемый модуль — последним */ + +/* ------------------------------------------------------------------------- + * setUp / tearDown + * ---------------------------------------------------------------------- */ + +void setUp(void) +{ + RESET_FAKE(GPIO_ReadPinInput); + FFF_RESET_HISTORY(); + /* Дефолт: кнопки не нажаты (пин HIGH) */ + GPIO_ReadPinInput_fake.return_val = 1U; + bsp_button_init(); +} + +void tearDown(void) +{ +} + +/* ------------------------------------------------------------------------- + * Вспомогательные функции + * ---------------------------------------------------------------------- */ + +/** Выполнить N вызовов bsp_button_poll() с одним и тем же raw-значением. + * Удобно для набора debounce-счётчика. */ +static void poll_n(uint32_t raw_val, uint32_t n) +{ + GPIO_ReadPinInput_fake.return_val = raw_val; + for (uint32_t i = 0U; i < n; i++) + { + bsp_button_poll(); + } +} + +/* ------------------------------------------------------------------------- + * Инициализация + * ---------------------------------------------------------------------- */ + +void test_init_returns_ok(void) +{ + bsp_status_t status = bsp_button_init(); + TEST_ASSERT_EQUAL(BSP_OK, status); +} + +void test_init_clears_state(void) +{ + /* Накопить состояние */ + poll_n(0U, 4U); /* 4 × LOW — debounce сработал */ + TEST_ASSERT_TRUE(bsp_button_is_pressed(BSP_BUTTON_1)); + + /* Реинициализация должна сбросить всё */ + bsp_button_init(); + TEST_ASSERT_FALSE(bsp_button_is_pressed(BSP_BUTTON_1)); + TEST_ASSERT_FALSE(bsp_button_get_event_pressed(BSP_BUTTON_1)); +} + +/* ------------------------------------------------------------------------- + * Сырое чтение (bsp_button_read) + * ---------------------------------------------------------------------- */ + +void test_read_returns_true_when_pin_low(void) +{ + GPIO_ReadPinInput_fake.return_val = 0U; /* LOW = нажата */ + TEST_ASSERT_TRUE(bsp_button_read(BSP_BUTTON_1)); +} + +void test_read_returns_false_when_pin_high(void) +{ + GPIO_ReadPinInput_fake.return_val = 1U; /* HIGH = не нажата */ + TEST_ASSERT_FALSE(bsp_button_read(BSP_BUTTON_1)); +} + +void test_read_invalid_index_returns_false(void) +{ + TEST_ASSERT_FALSE(bsp_button_read((bsp_button_t) BSP_BUTTON_COUNT)); + TEST_ASSERT_FALSE(bsp_button_read((bsp_button_t) 99U)); +} + +/* ------------------------------------------------------------------------- + * Debounce — событие нажатия + * ---------------------------------------------------------------------- */ + +void test_debounce_no_event_before_threshold(void) +{ + /* 3 сэмпла LOW — порог 4 не достигнут */ + poll_n(0U, 3U); + TEST_ASSERT_FALSE(bsp_button_get_event_pressed(BSP_BUTTON_1)); + TEST_ASSERT_FALSE(bsp_button_is_pressed(BSP_BUTTON_1)); +} + +void test_debounce_event_fires_on_4th_sample(void) +{ + poll_n(0U, 4U); + TEST_ASSERT_TRUE(bsp_button_get_event_pressed(BSP_BUTTON_1)); +} + +void test_debounce_stable_state_after_threshold(void) +{ + poll_n(0U, 4U); + TEST_ASSERT_TRUE(bsp_button_is_pressed(BSP_BUTTON_1)); +} + +void test_debounce_no_duplicate_event_on_hold(void) +{ + /* Порог достигнут, затем кнопка удерживается ещё много раз */ + poll_n(0U, 4U); + (void) bsp_button_get_event_pressed(BSP_BUTTON_1); /* сброс флага */ + + poll_n(0U, 10U); /* удержание */ + TEST_ASSERT_FALSE(bsp_button_get_event_pressed(BSP_BUTTON_1)); +} + +/* ------------------------------------------------------------------------- + * Потребление события + * ---------------------------------------------------------------------- */ + +void test_event_consumed_after_get(void) +{ + poll_n(0U, 4U); + TEST_ASSERT_TRUE(bsp_button_get_event_pressed(BSP_BUTTON_1)); + /* Второй вызов — флаг уже сброшен */ + TEST_ASSERT_FALSE(bsp_button_get_event_pressed(BSP_BUTTON_1)); +} + +/* ------------------------------------------------------------------------- + * Debounce — событие отпускания + * ---------------------------------------------------------------------- */ + +void test_debounce_released_event(void) +{ + /* Нажать */ + poll_n(0U, 4U); + (void) bsp_button_get_event_pressed(BSP_BUTTON_1); + + /* Отпустить */ + poll_n(1U, 4U); + TEST_ASSERT_TRUE(bsp_button_get_event_released(BSP_BUTTON_1)); + TEST_ASSERT_FALSE(bsp_button_is_pressed(BSP_BUTTON_1)); +} + +void test_debounce_released_event_consumed(void) +{ + poll_n(0U, 4U); + poll_n(1U, 4U); + TEST_ASSERT_TRUE(bsp_button_get_event_released(BSP_BUTTON_1)); + TEST_ASSERT_FALSE(bsp_button_get_event_released(BSP_BUTTON_1)); +} + +/* ------------------------------------------------------------------------- + * Сброс счётчика при глитче + * ---------------------------------------------------------------------- */ + +void test_counter_reset_on_glitch(void) +{ + /* 3 сэмпла LOW — почти порог */ + poll_n(0U, 3U); + /* Один HIGH — глитч, счётчик сбрасывается */ + poll_n(1U, 1U); + /* Ещё 3 LOW — снова не хватает одного */ + poll_n(0U, 3U); + TEST_ASSERT_FALSE(bsp_button_get_event_pressed(BSP_BUTTON_1)); + TEST_ASSERT_FALSE(bsp_button_is_pressed(BSP_BUTTON_1)); + + /* Четвёртый сэмпл LOW — теперь порог достигнут */ + poll_n(0U, 1U); + TEST_ASSERT_TRUE(bsp_button_get_event_pressed(BSP_BUTTON_1)); +} + +/* ------------------------------------------------------------------------- + * Независимость кнопок + * ---------------------------------------------------------------------- */ + +/* + * GPIO_ReadPinInput вызывается для обеих кнопок в одном bsp_button_poll(). + * Чтобы задать разные значения для BTN1 и BTN2, используем return_val_seq: + * каждый последующий вызов возвращает следующий элемент массива. + * При poll_n(n=4): 4 вызова × 2 кнопки = 8 значений в последовательности. + */ +void test_no_crosstalk_btn2_pressed_btn1_idle(void) +{ + /* Чередуем: BTN1=HIGH(не нажата), BTN2=LOW(нажата) */ + uint32_t seq[] = { + 1U, 0U, /* poll 1: btn1=HIGH, btn2=LOW */ + 1U, 0U, /* poll 2 */ + 1U, 0U, /* poll 3 */ + 1U, 0U, /* poll 4 */ + }; + SET_RETURN_SEQ(GPIO_ReadPinInput, seq, 8); + + for (uint32_t i = 0U; i < 4U; i++) + { + bsp_button_poll(); + } + + TEST_ASSERT_FALSE(bsp_button_get_event_pressed(BSP_BUTTON_1)); + TEST_ASSERT_TRUE(bsp_button_get_event_pressed(BSP_BUTTON_2)); +} + +void test_both_buttons_independent_state(void) +{ + /* Нажать BTN1, не нажимать BTN2 */ + uint32_t seq_press[] = { + 0U, 1U, 0U, 1U, 0U, 1U, 0U, 1U, + }; + SET_RETURN_SEQ(GPIO_ReadPinInput, seq_press, 8); + for (uint32_t i = 0U; i < 4U; i++) + { + bsp_button_poll(); + } + + TEST_ASSERT_TRUE(bsp_button_is_pressed(BSP_BUTTON_1)); + TEST_ASSERT_FALSE(bsp_button_is_pressed(BSP_BUTTON_2)); +} + +/* ------------------------------------------------------------------------- + * Граничные значения индекса + * ---------------------------------------------------------------------- */ + +void test_is_pressed_invalid_index_returns_false(void) +{ + TEST_ASSERT_FALSE(bsp_button_is_pressed((bsp_button_t) BSP_BUTTON_COUNT)); +} + +void test_get_event_pressed_invalid_index_returns_false(void) +{ + TEST_ASSERT_FALSE(bsp_button_get_event_pressed((bsp_button_t) 99U)); +} + +void test_get_event_released_invalid_index_returns_false(void) +{ + TEST_ASSERT_FALSE(bsp_button_get_event_released((bsp_button_t) 99U)); +} + +/* ------------------------------------------------------------------------- + * main + * ---------------------------------------------------------------------- */ + +int main(void) +{ + UNITY_BEGIN(); + + /* Инициализация */ + RUN_TEST(test_init_returns_ok); + RUN_TEST(test_init_clears_state); + + /* Сырое чтение */ + RUN_TEST(test_read_returns_true_when_pin_low); + RUN_TEST(test_read_returns_false_when_pin_high); + RUN_TEST(test_read_invalid_index_returns_false); + + /* Debounce — нажатие */ + RUN_TEST(test_debounce_no_event_before_threshold); + RUN_TEST(test_debounce_event_fires_on_4th_sample); + RUN_TEST(test_debounce_stable_state_after_threshold); + RUN_TEST(test_debounce_no_duplicate_event_on_hold); + + /* Потребление */ + RUN_TEST(test_event_consumed_after_get); + + /* Debounce — отпускание */ + RUN_TEST(test_debounce_released_event); + RUN_TEST(test_debounce_released_event_consumed); + + /* Глитч */ + RUN_TEST(test_counter_reset_on_glitch); + + /* Независимость */ + RUN_TEST(test_no_crosstalk_btn2_pressed_btn1_idle); + RUN_TEST(test_both_buttons_independent_state); + + /* Граничные индексы */ + RUN_TEST(test_is_pressed_invalid_index_returns_false); + RUN_TEST(test_get_event_pressed_invalid_index_returns_false); + RUN_TEST(test_get_event_released_invalid_index_returns_false); + + return UNITY_END(); +} \ No newline at end of file diff --git a/tests/host/mocks/fsl_gpio.h b/tests/host/mocks/fsl_gpio.h index c2ac53b..a88963c 100644 --- a/tests/host/mocks/fsl_gpio.h +++ b/tests/host/mocks/fsl_gpio.h @@ -76,7 +76,7 @@ typedef struct void GPIO_PinInit(GPIO_Type *base, uint32_t pin, const gpio_pin_config_t *config); void GPIO_PinWrite(GPIO_Type *base, uint32_t pin, uint8_t output); uint32_t GPIO_PinRead(GPIO_Type *base, uint32_t pin); - +uint32_t GPIO_ReadPinInput(GPIO_Type *base, uint32_t pin); void GPIO_SetPinInterruptConfig(GPIO_Type *base, uint32_t pin, gpio_interrupt_mode_t pinInterruptMode); void GPIO_EnableInterrupts(GPIO_Type *base, uint32_t mask); diff --git a/tests/host/mocks/pin_mux.h b/tests/host/mocks/pin_mux.h index f61f861..9eac7ce 100644 --- a/tests/host/mocks/pin_mux.h +++ b/tests/host/mocks/pin_mux.h @@ -16,4 +16,22 @@ /* UserLed2 → GPIO3 pin 4 (LED_APP) */ #define BOARD_INITPINS_UserLed2_GPIO GPIO3 #define BOARD_INITPINS_UserLed2_GPIO_PIN 4U -#define BOARD_INITPINS_UserLed2_INIT_GPIO_VALUE 1U \ No newline at end of file +#define BOARD_INITPINS_UserLed2_INIT_GPIO_VALUE 1U + +/* GPIO_B1_14 (coord C14), TactBut1 */ +/* Routed pin properties */ +#define BOARD_INITPINS_TactBut1_PERIPHERAL GPIO2 /*!< Peripheral name */ +#define BOARD_INITPINS_TactBut1_SIGNAL gpio_io /*!< Signal name */ +#define BOARD_INITPINS_TactBut1_CHANNEL 30U /*!< Signal channel */ + +/* Symbols to be used with GPIO driver */ +#define BOARD_INITPINS_TactBut1_GPIO \ + GPIO2 /*!< GPIO peripheral base pointer \ + */ +#define BOARD_INITPINS_TactBut1_GPIO_PIN 30U /*!< GPIO pin number */ + +/* Symbols to be used with GPIO driver */ +#define BOARD_INITPINS_TactBut2_GPIO \ + GPIO2 /*!< GPIO peripheral base pointer \ + */ +#define BOARD_INITPINS_TactBut2_GPIO_PIN 31U /*!< GPIO pin number */ diff --git a/tests/target/CMakeLists.txt b/tests/target/CMakeLists.txt index cd58b2b..d79766f 100644 --- a/tests/target/CMakeLists.txt +++ b/tests/target/CMakeLists.txt @@ -1,3 +1,4 @@ add_subdirectory(host_uart) add_subdirectory(hil_opto) add_subdirectory(hil_can) +add_subdirectory(hil_button) diff --git a/tests/target/hil_button/CMakeLists.txt b/tests/target/hil_button/CMakeLists.txt new file mode 100644 index 0000000..d8026fc --- /dev/null +++ b/tests/target/hil_button/CMakeLists.txt @@ -0,0 +1,21 @@ +set(TARGET_NAME test_hil_button) + +add_executable(${TARGET_NAME} main.c ${BSP_GENERATED}/clock_config.c + ${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE}) + +target_link_options( + ${TARGET_NAME} + PRIVATE + -T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_ram.ld + -Wl,--gc-sections + -Wl,--print-memory-usage + -Wl,-Map=${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}.map) + +target_link_libraries(${TARGET_NAME} PRIVATE bsp_boot_ram bsp_board bsp_button + bsp_led bsp_tick bsp_uart_host) + +add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND ${CMAKE_SIZE} $ + COMMENT "Size: ${TARGET_NAME}") diff --git a/tests/target/hil_button/main.c b/tests/target/hil_button/main.c new file mode 100644 index 0000000..deaee82 --- /dev/null +++ b/tests/target/hil_button/main.c @@ -0,0 +1,178 @@ +#include "board.h" +#include "bsp/button.h" +#include "bsp/led.h" +#include "bsp/tick.h" +#include "bsp/uart_host.h" + +#include + +#define CLI_BAUD_RATE 115200U +#define CLI_LINE_MAX 64U +#define CLI_RX_TIMEOUT 50U /* мс */ +#define POLL_PERIOD_MS 5U /* интервал debounce-poll */ + +/* ------------------------------------------------------------------------- + * CLI — чтение строки + * ---------------------------------------------------------------------- */ + +static size_t cli_read_line(uint8_t *p_buf, size_t max_len) +{ + size_t pos = 0U; + while (pos < (max_len - 1U)) + { + int32_t byte = bsp_uart_host_read_byte(CLI_RX_TIMEOUT); + if (byte < 0) + { + break; + } + if ((char) byte == '\r') + { + continue; + } + if ((char) byte == '\n') + { + break; + } + p_buf[pos++] = (uint8_t) byte; + } + p_buf[pos] = '\0'; + return pos; +} + +/* ------------------------------------------------------------------------- + * CLI — обработка команды + * + * Команды: + * PING → PONG + * READ <0|1> → 1 (нажата) | 0 (не нажата) — сырое чтение + * STATE <0|1> → 1 | 0 — стабильное состояние после debounce + * POLL → OK — один шаг debounce (дополнительно к фоновому) + * EVENT_P <0|1> → 1 | 0 — get_event_pressed, сбрасывает флаг + * EVENT_R <0|1> → 1 | 0 — get_event_released, сбрасывает флаг + * ---------------------------------------------------------------------- */ + +static void cli_process(const char *line) +{ + if (strcmp(line, "PING") == 0) + { + bsp_uart_host_write_str("PONG\r\n"); + return; + } + + if (strcmp(line, "POLL") == 0) + { + bsp_button_poll(); + bsp_uart_host_write_str("OK\r\n"); + return; + } + + /* READ */ + if (strncmp(line, "READ ", 5U) == 0) + { + uint32_t idx = (uint32_t) (line[5] - '0'); + if (idx < (uint32_t) BSP_BUTTON_COUNT) + { + bsp_uart_host_write_str(bsp_button_read((bsp_button_t) idx) ? "1\r\n" : "0\r\n"); + } + else + { + bsp_uart_host_write_str("ERR_IDX\r\n"); + } + return; + } + + /* STATE */ + if (strncmp(line, "STATE ", 6U) == 0) + { + uint32_t idx = (uint32_t) (line[6] - '0'); + if (idx < (uint32_t) BSP_BUTTON_COUNT) + { + bsp_uart_host_write_str(bsp_button_is_pressed((bsp_button_t) idx) ? "1\r\n" : "0\r\n"); + } + else + { + bsp_uart_host_write_str("ERR_IDX\r\n"); + } + return; + } + + /* EVENT_P */ + if (strncmp(line, "EVENT_P ", 8U) == 0) + { + uint32_t idx = (uint32_t) (line[8] - '0'); + if (idx < (uint32_t) BSP_BUTTON_COUNT) + { + bsp_uart_host_write_str(bsp_button_get_event_pressed((bsp_button_t) idx) ? "1\r\n" + : "0\r\n"); + } + else + { + bsp_uart_host_write_str("ERR_IDX\r\n"); + } + return; + } + + /* EVENT_R */ + if (strncmp(line, "EVENT_R ", 8U) == 0) + { + uint32_t idx = (uint32_t) (line[8] - '0'); + if (idx < (uint32_t) BSP_BUTTON_COUNT) + { + bsp_uart_host_write_str(bsp_button_get_event_released((bsp_button_t) idx) ? "1\r\n" + : "0\r\n"); + } + else + { + bsp_uart_host_write_str("ERR_IDX\r\n"); + } + return; + } + + if (line[0] != '\0') + { + bsp_uart_host_write_str("ERR_UNKNOWN\r\n"); + } +} + +/* ------------------------------------------------------------------------- + * main + * ---------------------------------------------------------------------- */ + +int main(void) +{ + board_hw_init(); + bsp_tick_init(); + bsp_led_init(); + bsp_uart_host_init(CLI_BAUD_RATE); + bsp_button_init(); + + bsp_led_on(LED_APP); + + /* Шлём READY пока хост не открыл порт */ + while (bsp_uart_host_rx_available() == 0U) + { + bsp_uart_host_write_str("READY\r\n"); + bsp_delay(200U); + } + + uint32_t last_poll_ms = bsp_tick_get_ms(); + static uint8_t s_line[CLI_LINE_MAX]; + + for (;;) + { + /* Фоновый debounce-poll каждые POLL_PERIOD_MS */ + uint32_t now = bsp_tick_get_ms(); + if ((now - last_poll_ms) >= POLL_PERIOD_MS) + { + bsp_button_poll(); + last_poll_ms = now; + } + + /* Обработка команды если пришла строка */ + size_t len = cli_read_line(s_line, sizeof(s_line)); + if (len > 0U) + { + cli_process((const char *) s_line); + } + } +} \ No newline at end of file diff --git a/tools/hil/04_test_button.py b/tools/hil/04_test_button.py new file mode 100644 index 0000000..7318430 --- /dev/null +++ b/tools/hil/04_test_button.py @@ -0,0 +1,136 @@ +"""test_hil_button.py — интерактивный HIL-тест bsp_button. + +Тип: базовый (без M5StampPLC). +Кнопки расположены на плате таргета — оператор нажимает их вручную +по подсказкам. Запускать с флагом -s: + + just host::hil-button + # или вручную: + uv run --directory tools/hil pytest test_hil_button.py -v -s +""" + +import time + +import pytest + +from conftest import uart_cmd + +# Время после нажатия/отпускания, чтобы debounce (20 мс) гарантированно сработал. +# Берём с запасом: 4 × 5 мс poll + накладные расходы CLI. +DEBOUNCE_SETTLE_S = 0.10 + + +def _operator_prompt(msg: str) -> None: + """Печатает подсказку оператору и ждёт Enter.""" + input(f"\n >>> {msg}\n Нажмите Enter когда готово...") + time.sleep(DEBOUNCE_SETTLE_S) + +@pytest.mark.interactive +class TestHilButton: + + @pytest.fixture(autouse=True) + def _setup(self, uart_button): + self.ser = uart_button + + # ------------------------------------------------------------------ + # Базовая связь + # ------------------------------------------------------------------ + + def test_ping(self): + """Проверка UART-канала.""" + assert uart_cmd(self.ser, "PING") == "PONG" + + # ------------------------------------------------------------------ + # Сырое чтение в покое + # ------------------------------------------------------------------ + + def test_button1_idle_raw(self): + """Кнопка 1 не нажата → READ 0 == 0.""" + _operator_prompt("Убедись что кнопка 1 (TactBut1) НЕ нажата") + assert uart_cmd(self.ser, "READ 0") == "0" + + def test_button2_idle_raw(self): + """Кнопка 2 не нажата → READ 1 == 0.""" + _operator_prompt("Убедись что кнопка 2 (TactBut2) НЕ нажата") + assert uart_cmd(self.ser, "READ 1") == "0" + + # ------------------------------------------------------------------ + # Сырое чтение при нажатии + # ------------------------------------------------------------------ + + def test_button1_raw_press(self): + """Нажатая кнопка 1 → READ 0 == 1.""" + _operator_prompt("Нажми и УДЕРЖИ кнопку 1 (TactBut1)") + assert uart_cmd(self.ser, "READ 0") == "1" + + def test_button1_raw_release(self): + """После отпускания → READ 0 == 0.""" + _operator_prompt("ОТПУСТИ кнопку 1 (TactBut1)") + assert uart_cmd(self.ser, "READ 0") == "0" + + def test_button2_raw_press(self): + """Нажатая кнопка 2 → READ 1 == 1.""" + _operator_prompt("Нажми и УДЕРЖИ кнопку 2 (TactBut2)") + assert uart_cmd(self.ser, "READ 1") == "1" + + def test_button2_raw_release(self): + """После отпускания → READ 1 == 0.""" + _operator_prompt("ОТПУСТИ кнопку 2 (TactBut2)") + assert uart_cmd(self.ser, "READ 1") == "0" + + # ------------------------------------------------------------------ + # Debounce: событие нажатия + # ------------------------------------------------------------------ + + def test_button1_debounce_pressed_event(self): + """Нажать и отпустить кнопку 1 → EVENT_P 0 == 1.""" + _operator_prompt("Нажми и отпусти кнопку 1 (TactBut1)") + assert uart_cmd(self.ser, "EVENT_P 0") == "1" + + def test_button1_event_consumed(self): + """EVENT_P сбрасывается после первого вызова.""" + # Флаг должен быть уже сброшен предыдущим тестом + assert uart_cmd(self.ser, "EVENT_P 0") == "0" + + def test_button2_debounce_pressed_event(self): + """Нажать и отпустить кнопку 2 → EVENT_P 1 == 1.""" + _operator_prompt("Нажми и отпусти кнопку 2 (TactBut2)") + assert uart_cmd(self.ser, "EVENT_P 1") == "1" + + # ------------------------------------------------------------------ + # Debounce: событие отпускания + # ------------------------------------------------------------------ + + def test_button1_released_event(self): + """Нажать, удержать, отпустить → EVENT_R 0 == 1.""" + _operator_prompt("Нажми кнопку 1 (TactBut1), подожди секунду, затем отпусти") + assert uart_cmd(self.ser, "EVENT_R 0") == "1" + + def test_button1_released_event_consumed(self): + """EVENT_R сбрасывается после первого вызова.""" + assert uart_cmd(self.ser, "EVENT_R 0") == "0" + + # ------------------------------------------------------------------ + # Стабильное состояние + # ------------------------------------------------------------------ + + def test_button1_stable_state_while_held(self): + """STATE 0 == 1 пока кнопка удерживается.""" + _operator_prompt("Нажми и УДЕРЖИ кнопку 1 (TactBut1)") + assert uart_cmd(self.ser, "STATE 0") == "1" + + def test_button1_stable_state_after_release(self): + """STATE 0 == 0 после отпускания.""" + _operator_prompt("ОТПУСТИ кнопку 1 (TactBut1)") + assert uart_cmd(self.ser, "STATE 0") == "0" + + # ------------------------------------------------------------------ + # Независимость кнопок + # ------------------------------------------------------------------ + + def test_buttons_independent_btn2_only(self): + """Нажата только кнопка 2 → STATE 0 == 0, STATE 1 == 1.""" + _operator_prompt("Нажми и УДЕРЖИ ТОЛЬКО кнопку 2 (TactBut2), кнопка 1 свободна") + assert uart_cmd(self.ser, "STATE 0") == "0", "Кнопка 1 не должна быть нажата" + assert uart_cmd(self.ser, "STATE 1") == "1", "Кнопка 2 должна быть нажата" + _operator_prompt("ОТПУСТИ кнопку 2 (TactBut2)") \ No newline at end of file diff --git a/tools/hil/conftest.py b/tools/hil/conftest.py index 528413d..3b8665f 100644 --- a/tools/hil/conftest.py +++ b/tools/hil/conftest.py @@ -226,6 +226,12 @@ def loaded_host_uart(request: pytest.FixtureRequest, m5: M5Agent) -> None: Path(cfg.BUILD_DIR) / "tests/target/host_uart/test_host_uart.elf", ) +@pytest.fixture(scope="module") +def loaded_hil_button(request: pytest.FixtureRequest, m5: M5Agent) -> None: + _load_elf( + request, + Path(cfg.BUILD_DIR) / "tests/target/hil_button/test_hil_button.elf", + ) @pytest.fixture(scope="module") def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None: @@ -286,6 +292,15 @@ def uart_can( ser = _open_uart_and_wait_ready(request) yield ser ser.close() + +@pytest.fixture(scope="module") +def uart_button( + request: pytest.FixtureRequest, + loaded_hil_button, +) -> Generator[serial.Serial, None, None]: + ser = _open_uart_and_wait_ready(request) + yield ser + ser.close() # --------------------------------------------------------------------------- # Утилита для тестов # --------------------------------------------------------------------------- diff --git a/tools/hil/pyproject.toml b/tools/hil/pyproject.toml index dfe7a49..5b385b4 100644 --- a/tools/hil/pyproject.toml +++ b/tools/hil/pyproject.toml @@ -13,9 +13,9 @@ dependencies = [ [tool.pytest.ini_options] testpaths = ["."] python_files = ["test_*.py", "*_test.py", "??_test_*.py"] - markers = [ "gpio: тесты GPIO", + "interactive: тест требует действий оператора", "uart: тесты UART CLI", "opto: тесты оптоизолированных входов", "m5: тесты с участием M5StampPLC",