# Created HIL & Host tests for bsp_button module
This commit is contained in:
parent
6c23315e1e
commit
8d5e3090b8
22 changed files with 1330 additions and 34 deletions
|
|
@ -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"
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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
15
bsp/button/CMakeLists.txt
Normal 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
224
bsp/button/README.md
Normal 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
|
||||||
|
около 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 <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
|
||||||
|
)
|
||||||
|
```
|
||||||
88
bsp/button/include/bsp/button.h
Normal file
88
bsp/button/include/bsp/button.h
Normal 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
146
bsp/button/src/button.c
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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`
|
||||||
|
|
||||||
|
|
@ -88,13 +89,18 @@ M5 → хост: {"ok": true, "opto_ch": 1, "relay": 3, "state": true}\r\n
|
||||||
**Доступные команды агента:**
|
**Доступные команды агента:**
|
||||||
|
|
||||||
| Команда | Параметры | Действие |
|
| Команда | Параметры | Действие |
|
||||||
|----------------|------------------------|---------------------------------------|
|
|----------------|------------------------------------|---------------------------------------------------|
|
||||||
| `ping` | — | Проверка связи |
|
| `ping` | — | Проверка связи |
|
||||||
|
| `info` | — | Версия, состояние CAN и AW9523, статус реле |
|
||||||
| `power` | `state: bool` | RLY1 — питание таргета |
|
| `power` | `state: bool` | RLY1 — питание таргета |
|
||||||
| `relay_set` | `ch: 1-4, state: bool` | Прямое управление реле |
|
| `relay_set` | `ch: 1-4, state: bool` | Прямое управление реле |
|
||||||
|
| `relay_get` | `ch: 1-4` | Прочитать текущее состояние реле |
|
||||||
| `relay_all_off`| — | Выключить все реле |
|
| `relay_all_off`| — | Выключить все реле |
|
||||||
| `opto_set` | `ch: 1-3, state: bool` | Управление оптоканалом таргета (через маппинг) |
|
| `opto_set` | `ch: 1-3, state: bool` | Управление оптоканалом таргета (через маппинг) |
|
||||||
| `opto_all_off` | — | Выключить все оптоканалы |
|
| `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 # интерактивный — нажатие кнопок оператором
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Добавление нового сигнала стенда
|
## Добавление нового сигнала стенда
|
||||||
|
|
|
||||||
|
|
@ -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 в выборке (маркер исключает)
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -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 — Интерпретация результатов
|
||||||
|
|
||||||
### Зелёный прогон
|
### Зелёный прогон
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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})
|
||||||
|
|
|
||||||
295
tests/host/button/test_bsp_button.c
Normal file
295
tests/host/button/test_bsp_button.c
Normal 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();
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
21
tests/target/hil_button/CMakeLists.txt
Normal file
21
tests/target/hil_button/CMakeLists.txt
Normal 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}")
|
||||||
178
tests/target/hil_button/main.c
Normal file
178
tests/target/hil_button/main.c
Normal 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
136
tools/hil/04_test_button.py
Normal 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)")
|
||||||
|
|
@ -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()
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Утилита для тестов
|
# Утилита для тестов
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue