# Created HIL & Host tests for bsp_button module

This commit is contained in:
Dmitry Akimov 2026-03-31 18:25:45 +03:00
parent 6c23315e1e
commit 8d5e3090b8
22 changed files with 1330 additions and 34 deletions

View file

@ -142,6 +142,7 @@
"test_bsp_led", "test_bsp_led",
"test_log", "test_log",
"test_bsp_opto", "test_bsp_opto",
"test_bsp_button",
"test_bsp_can", "test_bsp_can",
"uart_host_mock_example", "uart_host_mock_example",
"test_ring_buffer", "test_ring_buffer",
@ -156,9 +157,10 @@
"targets": [ "targets": [
"test_bsp_led", "test_bsp_led",
"test_log", "test_log",
"test_bsp_opto",
"test_bsp_button",
"test_bsp_can", "test_bsp_can",
"uart_host_mock_example", "uart_host_mock_example",
"test_bsp_opto",
"test_ring_buffer", "test_ring_buffer",
"test_timeout_pattern", "test_timeout_pattern",
"uart_host_mock_example" "uart_host_mock_example"
@ -170,6 +172,7 @@
"configurePreset": "target-debug", "configurePreset": "target-debug",
"targets": [ "targets": [
"test_host_uart", "test_host_uart",
"test_hil_button",
"test_hil_can", "test_hil_can",
"test_hil_opto" "test_hil_opto"
] ]

View file

@ -14,6 +14,8 @@ add_subdirectory(tick)
add_subdirectory(uart_host) add_subdirectory(uart_host)
add_subdirectory(opto) add_subdirectory(opto)
add_subdirectory(can) add_subdirectory(can)
add_subdirectory(button)
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки # Подавляем предупреждения при компиляции собственных .c файлов библиотеки
target_compile_options(bsp_board PRIVATE -w) target_compile_options(bsp_board PRIVATE -w)

15
bsp/button/CMakeLists.txt Normal file
View file

@ -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)

224
bsp/button/README.md Normal file
View file

@ -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
около 50100 мс.
---
## Быстрый старт
```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 (рекомендуется 1530 мс).
---
## Тестирование
### 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 <idx>` | `1` / `0` | Сырое состояние (без debounce) |
| `STATE <idx>`| `1` / `0` | Стабильное состояние после debounce |
| `EVENT_P <idx>`| `1` / `0` | `get_event_pressed`, сбрасывает флаг |
| `EVENT_R <idx>`| `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
)
```

View file

@ -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 <stdbool.h>
#include <stdint.h>
/* -------------------------------------------------------------------------
* Типы
* ---------------------------------------------------------------------- */
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 */

146
bsp/button/src/button.c Normal file
View file

@ -0,0 +1,146 @@
#include "bsp/button.h"
#include "bsp/status.h"
#include "fsl_gpio.h"
#include "pin_mux.h"
#include <stddef.h>
#include <string.h>
/* -------------------------------------------------------------------------
* Конфигурация 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;
}

View file

@ -39,6 +39,7 @@ M5: реле AW9523B через ULN2003A, нормально разомкнут
| RLY4 | EXT_IN2 | `BSP_OPTO_CH_IN2` | GPIO_AD_B1_05 | GPIO1[21] | | RLY4 | EXT_IN2 | `BSP_OPTO_CH_IN2` | GPIO_AD_B1_05 | GPIO1[21] |
**Логика сигнала:** **Логика сигнала:**
- Реле разомкнуто → нет тока через оптопару → пин LOW → `BSP_OPTO_STATE_INACTIVE` - Реле разомкнуто → нет тока через оптопару → пин LOW → `BSP_OPTO_STATE_INACTIVE`
- Реле замкнуто → ток через оптопару → пин HIGH → `BSP_OPTO_STATE_ACTIVE` - Реле замкнуто → ток через оптопару → пин HIGH → `BSP_OPTO_STATE_ACTIVE`
@ -87,14 +88,19 @@ M5 → хост: {"ok": true, "opto_ch": 1, "relay": 3, "state": true}\r\n
**Доступные команды агента:** **Доступные команды агента:**
| Команда | Параметры | Действие | | Команда | Параметры | Действие |
|----------------|------------------------|---------------------------------------| |----------------|------------------------------------|---------------------------------------------------|
| `ping` | — | Проверка связи | | `ping` | — | Проверка связи |
| `power` | `state: bool` | RLY1 — питание таргета | | `info` | — | Версия, состояние CAN и AW9523, статус реле |
| `relay_set` | `ch: 1-4, state: bool` | Прямое управление реле | | `power` | `state: bool` | RLY1 — питание таргета |
| `relay_all_off`| — | Выключить все реле | | `relay_set` | `ch: 1-4, state: bool` | Прямое управление реле |
| `opto_set` | `ch: 1-3, state: bool` | Управление оптоканалом таргета (через маппинг) | | `relay_get` | `ch: 1-4` | Прочитать текущее состояние реле |
| `opto_all_off` | — | Выключить все оптоканалы | | `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:** **Деплой агента на M5:**
@ -131,7 +137,7 @@ pytest → pyserial → MCU-Link VCOM → LPUART1 → прошивка тарг
`tools/hil/conftest.py` содержит всю логику подготовки стенда: `tools/hil/conftest.py` содержит всю логику подготовки стенда:
``` ```bash
фикстура m5 (scope=module) фикстура m5 (scope=module)
├── подключиться к M5 агенту ├── подключиться к M5 агенту
├── agent.power(True) ← RLY1: питание таргета ON ├── agent.power(True) ← RLY1: питание таргета ON
@ -176,6 +182,22 @@ just host::m5-power off
just host::uart-monitor 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 # интерактивный — нажатие кнопок оператором
```
--- ---
## Добавление нового сигнала стенда ## Добавление нового сигнала стенда

View file

@ -17,12 +17,15 @@ just/build.just
build-hil build-hil
``` ```
Два типа тестов: Три типа тестов:
| Тип | Использует M5 | Когда применять | | Тип | Использует M5 | Запуск | Когда применять |
|-----|--------------|-----------------| |-----|--------------|--------|-----------------|
| **Базовый** | Нет | Тестирование UART CLI, алгоритмов, таймингов | | **Базовый** | Нет | `hil-run` | Тестирование UART CLI, алгоритмов, таймингов |
| **С M5** | Да | Тестирование GPIO, оптовходов, реле, питания | | **С 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}'") raise TimeoutError(f"Ожидали {expected!r} от '{cmd}'")
``` ```
### Интерактивный тест (оператор нажимает кнопки / смотрит на дисплей)
Добавить маркер на класс. Для ввода использовать `/dev/tty` напрямую — `input()` не работает под захватом pytest даже с `-s`:
```python
"""test_<name>.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<Name>:
@pytest.fixture(autouse=True)
def _setup(self, uart_<name>):
self.ser = uart_<name>
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`: добавить рецепт (опционально) ## Шаг 7 — `just/host.just`: добавить рецепт (опционально)
@ -298,6 +349,16 @@ hil-<name>:
uv run --directory {{ HIL_DIR }} pytest test_<name>.py -v uv run --directory {{ HIL_DIR }} pytest test_<name>.py -v
``` ```
Для интерактивных — добавить флаг `-s`:
```just
[doc('Запустить HIL-тест <n> (интерактивный, требует оператора)')]
[group('hil')]
hil-<n>:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest test_<n>.py -s -v
```
--- ---
## Полный цикл ## Полный цикл
@ -361,14 +422,25 @@ uart.close() test_something
## Чеклист ## Чеклист
### Автоматический тест (базовый или с M5)
```bash ```bash
[ ] tests/target/<name>/main.c — C-прошивка с CLI + READY-паттерн [ ] tests/target/<n>/main.c — C-прошивка с CLI + READY-паттерн
[ ] tests/target/<name>/CMakeLists.txt — сборка с bsp_boot_ram [ ] tests/target/<n>/CMakeLists.txt — сборка с bsp_boot_ram
[ ] tests/target/CMakeLists.txt — add_subdirectory(<name>) [ ] tests/target/CMakeLists.txt — add_subdirectory(<n>)
[ ] CMakePresets.json — добавить test_<name> в targets [ ] CMakePresets.json — добавить test_<n> в targets
[ ] tools/hil/conftest.py — loaded_<name> + uart_<name> [ ] tools/hil/conftest.py — loaded_<n> + uart_<n>
[ ] tools/hil/test_<name>.py — pytest-тесты [ ] tools/hil/test_<n>.py — pytest-тесты
[ ] just/host.just — рецепт hil-<name> (опционально) [ ] just/host.just — рецепт hil-<n> (опционально)
[ ] just build::build-hil — зелёная сборка [ ] just build::build-hil — зелёная сборка
[ ] just host::hil-<name> — зелёный прогон [ ] just host::hil-<n> — зелёный прогон
```
### Интерактивный тест (дополнительно к базовому чеклисту)
```bash
[ ] @pytest.mark.interactive — пометить класс в test_<n>.py
[ ] just/host.just — рецепт hil-<n> с флагом -s
[ ] just host::hil-run-interactive — зелёный прогон
[ ] убедиться что just host::hil-run — NOT в выборке (маркер исключает)
``` ```

View file

@ -110,20 +110,31 @@ ls build/target-debug/tests/target/
## Шаг 4 — Запуск тестов (на хосте, вне контейнера) ## Шаг 4 — Запуск тестов (на хосте, вне контейнера)
### Все HIL-тесты ### Автоматические HIL-тесты (без оператора)
```bash ```bash
just host::hil-run just host::hil-run
``` ```
pytest обходит все `test_*.py` в `tools/hil/`, запускает их по очереди. pytest обходит все `test_*.py` в `tools/hil/`, **исключая** помеченные `@pytest.mark.interactive`.
Каждый файл — своя загрузка ELF, свой UART-сеанс, MCU перезагружается между файлами. Каждый файл — своя загрузка ELF, свой UART-сеанс, MCU перезагружается между файлами.
### Конкретный тест ### Интерактивные HIL-тесты (требуют оператора)
```bash
just host::hil-run-interactive # все интерактивные (кнопки, дисплей и т.п.)
just host::hil-button # конкретный интерактивный
```
Запускаются с флагом `-s` — pytest не перехватывает stdin/stdout, оператор видит
подсказки и может нажимать Enter. Не входят в `hil-run` и не запускаются в CI.
### Конкретный автоматический тест
```bash ```bash
just host::hil-uart # только test_uart.py just host::hil-uart # только test_uart.py
just host::hil-opto # только test_opto.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 и не нужно каждый раз Удобно при отладке тестов когда прошивка уже в RAM и не нужно каждый раз
ждать загрузки через pyOCD. ждать загрузки через pyOCD.
---
## Шаг 5 — Интерпретация результатов ## Шаг 5 — Интерпретация результатов
### Зелёный прогон ### Зелёный прогон

View file

@ -19,6 +19,7 @@ target_link_libraries(
bsp_led bsp_led
bsp_tick bsp_tick
bsp_can bsp_can
bsp_button
bsp_uart_host bsp_uart_host
bsp_boot_xip bsp_boot_xip
bsp_opto bsp_opto

View file

@ -1,5 +1,6 @@
#include "board.h" #include "board.h"
#include "bsp/button.h"
#include "bsp/can.h" #include "bsp/can.h"
#include "bsp/led.h" #include "bsp/led.h"
#include "bsp/opto.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) int main(void)
{ {
const uint16_t DELAY_MS = 10; const uint16_t DELAY_MS = 5;
const uint32_t UART_BAUDRATE = 115200; const uint32_t UART_BAUDRATE = 115200;
const uint32_t CAN_BAUDRATE = 125000; const uint32_t CAN_BAUDRATE = 125000;
board_hw_init(); board_hw_init();
bsp_button_init();
bsp_led_init(); bsp_led_init();
bsp_tick_init(); bsp_tick_init();
bsp_uart_host_init(UART_BAUDRATE); bsp_uart_host_init(UART_BAUDRATE);
@ -88,6 +90,27 @@ int main(void)
while (1) while (1)
{ {
bsp_opto_process(); 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) if (bsp_can_receive(&rx_frame, 0) == BSP_OK)
{ {

View file

@ -330,11 +330,17 @@ flash-swd-erase:
_hil_build := BUILD_DIR / "target-debug" _hil_build := BUILD_DIR / "target-debug"
[doc('Запустить все HIL-тесты')] [doc('Запустить все не интерактивные HIL-тесты')]
[group('hil')] [group('hil')]
hil-run: hil-run:
HIL_BUILD_DIR={{ _hil_build }} \ 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')] [doc('Запустить HIL-тест UART')]
[group('hil')] [group('hil')]
@ -348,6 +354,12 @@ hil-opto:
HIL_BUILD_DIR={{ _hil_build }} \ HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest 02_test_opto.py -v 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')] [doc('Запустить HIL тест bsp_can')]
[group('hil')] [group('hil')]
hil-can: hil-can:

View file

@ -143,3 +143,18 @@ add_host_test(
${CMAKE_SOURCE_DIR}/utils/ ${CMAKE_SOURCE_DIR}/utils/
MOCKS MOCKS
${BSP_MOCKS_DIR}) ${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})

View file

@ -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();
}

View file

@ -76,7 +76,7 @@ typedef struct
void GPIO_PinInit(GPIO_Type *base, uint32_t pin, const gpio_pin_config_t *config); 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); 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_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, void GPIO_SetPinInterruptConfig(GPIO_Type *base, uint32_t pin,
gpio_interrupt_mode_t pinInterruptMode); gpio_interrupt_mode_t pinInterruptMode);
void GPIO_EnableInterrupts(GPIO_Type *base, uint32_t mask); void GPIO_EnableInterrupts(GPIO_Type *base, uint32_t mask);

View file

@ -17,3 +17,21 @@
#define BOARD_INITPINS_UserLed2_GPIO GPIO3 #define BOARD_INITPINS_UserLed2_GPIO GPIO3
#define BOARD_INITPINS_UserLed2_GPIO_PIN 4U #define BOARD_INITPINS_UserLed2_GPIO_PIN 4U
#define BOARD_INITPINS_UserLed2_INIT_GPIO_VALUE 1U #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 */

View file

@ -1,3 +1,4 @@
add_subdirectory(host_uart) add_subdirectory(host_uart)
add_subdirectory(hil_opto) add_subdirectory(hil_opto)
add_subdirectory(hil_can) add_subdirectory(hil_can)
add_subdirectory(hil_button)

View file

@ -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} $<TARGET_FILE:${TARGET_NAME}>
COMMENT "Size: ${TARGET_NAME}")

View file

@ -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 <string.h>
#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 <idx> */
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 <idx> */
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 <idx> */
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 <idx> */
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);
}
}
}

136
tools/hil/04_test_button.py Normal file
View file

@ -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)")

View file

@ -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", 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") @pytest.fixture(scope="module")
def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None: def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None:
@ -286,6 +292,15 @@ def uart_can(
ser = _open_uart_and_wait_ready(request) ser = _open_uart_and_wait_ready(request)
yield ser yield ser
ser.close() 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()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Утилита для тестов # Утилита для тестов
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -13,9 +13,9 @@ dependencies = [
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["."] testpaths = ["."]
python_files = ["test_*.py", "*_test.py", "??_test_*.py"] python_files = ["test_*.py", "*_test.py", "??_test_*.py"]
markers = [ markers = [
"gpio: тесты GPIO", "gpio: тесты GPIO",
"interactive: тест требует действий оператора",
"uart: тесты UART CLI", "uart: тесты UART CLI",
"opto: тесты оптоизолированных входов", "opto: тесты оптоизолированных входов",
"m5: тесты с участием M5StampPLC", "m5: тесты с участием M5StampPLC",