From 6673bf5da6e6d1e0909bf4230ce5cac0a8691ea0 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Mon, 23 Mar 2026 13:58:09 +0300 Subject: [PATCH] # 11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - написан и покрыт хост-тестами модуль bsp_opto (оптовоходы RS, IN1, IN2) - TODO: написал первый HIL тест с m5stamplc --- .clang-tidy | 1 + .clangd | 1 + CMakePresets.json | 2 + bsp/CMakeLists.txt | 4 +- bsp/opto/CMakeLists.txt | 8 + bsp/opto/README.md | 69 +++++ bsp/opto/include/bsp/opto.h | 145 ++++++++++ bsp/opto/src/opto.c | 214 ++++++++++++++ bsp/tick/src/tick.c | 13 +- tests/HOST_CREATE_TEST.md | 6 +- tests/host/CMakeLists.txt | 18 +- tests/host/mocks/board.h | 13 +- tests/host/mocks/bsp/tick.h | 11 + tests/host/mocks/fsl_gpio.h | 64 ++++- tests/host/mocks/fsl_iomuxc.h | 8 + tests/host/mocks/pin_mux.h | 9 +- tests/host/opto/test_bsp_opto.c | 492 ++++++++++++++++++++++++++++++++ 17 files changed, 1049 insertions(+), 29 deletions(-) create mode 100644 bsp/opto/CMakeLists.txt create mode 100644 bsp/opto/README.md create mode 100644 bsp/opto/include/bsp/opto.h create mode 100644 bsp/opto/src/opto.c create mode 100644 tests/host/mocks/bsp/tick.h create mode 100644 tests/host/mocks/fsl_iomuxc.h create mode 100644 tests/host/opto/test_bsp_opto.c diff --git a/.clang-tidy b/.clang-tidy index 70ccfda..c29fbc9 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -10,6 +10,7 @@ Checks: > hicpp-signed-bitwise, performance-*, portability-*, + -performance-no-int-to-ptr, readability-braces-around-statements, readability-container-size-empty, readability-function-size, diff --git a/.clangd b/.clangd index 0a28179..7bc0daf 100644 --- a/.clangd +++ b/.clangd @@ -65,6 +65,7 @@ Diagnostics: - readability-* - portability-* Remove: + - performance-no-int-to-ptr - readability-magic-numbers - readability-isolate-declaration - bugprone-easily-swappable-parameters diff --git a/CMakePresets.json b/CMakePresets.json index 7105b85..cb24391 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -141,6 +141,7 @@ "targets": [ "test_bsp_led", "test_log", + "test_bsp_opto", "test_ring_buffer", "test_timeout_pattern", "uart_host_mock_example" @@ -153,6 +154,7 @@ "targets": [ "test_bsp_led", "test_log", + "test_bsp_opto", "test_ring_buffer", "test_timeout_pattern", "uart_host_mock_example" diff --git a/bsp/CMakeLists.txt b/bsp/CMakeLists.txt index 0b5c10d..1e553ef 100644 --- a/bsp/CMakeLists.txt +++ b/bsp/CMakeLists.txt @@ -3,7 +3,7 @@ if(BUILD_TESTS_HOST) endif() # ----------------------------------------------------------------------------- -# bsp_board — стартап, сгенерированные файлы Config Tools, инициализация платы +# bsp_board — генерированные файлы Config Tools, инициализация платы # ----------------------------------------------------------------------------- add_library(bsp_board STATIC generated/board.c generated/pin_mux.c) @@ -12,7 +12,7 @@ add_subdirectory(common) add_subdirectory(led) add_subdirectory(tick) add_subdirectory(uart_host) - +add_subdirectory(opto) # Подавляем предупреждения при компиляции собственных .c файлов библиотеки target_compile_options(bsp_board PRIVATE -w) diff --git a/bsp/opto/CMakeLists.txt b/bsp/opto/CMakeLists.txt new file mode 100644 index 0000000..9aeb809 --- /dev/null +++ b/bsp/opto/CMakeLists.txt @@ -0,0 +1,8 @@ +add_library(bsp_opto STATIC src/opto.c) + +target_include_directories(bsp_opto PUBLIC include) + +target_link_libraries( + bsp_opto + PUBLIC bsp_status + PRIVATE bsp_tick bsp_board sdk_gpio) diff --git a/bsp/opto/README.md b/bsp/opto/README.md new file mode 100644 index 0000000..b24f33e --- /dev/null +++ b/bsp/opto/README.md @@ -0,0 +1,69 @@ +# bsp_opto — оптоизолированные входы + +## Аппаратура + +| Канал | Пин MCU | GPIO | Схема | +|--------------------|------------------|------------|--------------------------| +| `BSP_OPTO_CH_IN1` | GPIO_AD_B1_06 | GPIO1[22] | PS2801-4 + 1K pull-up | +| `BSP_OPTO_CH_IN2` | GPIO_AD_B1_05 | GPIO1[21] | PS2801-4 + 1K pull-up | +| `BSP_OPTO_CH_RS` | GPIO_AD_B1_07 | GPIO1[23] | PS2801-4 + 1K pull-up | + +**Логика:** active-low. Пин HIGH = нет тока = `INACTIVE`. Пин LOW = ток течёт = `ACTIVE`. + +`BSP_OPTO_CH_RS` — опциональный. Активен только при `rs_as_gpio = true` в конфигурации. +При `rs_as_gpio = false` пин остаётся под управлением `bsp_uart_rs` (LPUART3_RX). + +Все три пина принадлежат GPIO1[16..31] → один IRQ: `GPIO1_Combined_16_31_IRQn`. + +## Дебаунс + +Аппаратного фильтра нет. Реализован программный дебаунс: + +1. ISR фиксирует timestamp (`bsp_tick_get_ms()`) и raw состояние пина, выставляет `pending`. +2. `bsp_opto_process()` вызывается из main loop. Если с момента последнего фронта прошло + >= `debounce_ms` — перечитывает пин, сравнивает с `confirmed_state`, стреляет коллбэком. + +Рекомендуемое значение `debounce_ms`: **10 мс** (PS2801-4 response ~50 мкс, +основной источник шума — механические контакты на стенде). + +## Использование + +```c +static void on_opto_change(bsp_opto_ch_t ch, bsp_opto_state_t state) +{ + if (ch == BSP_OPTO_CH_IN1 && state == BSP_OPTO_STATE_ACTIVE) { + /* IN1 активирован */ + } +} + +bsp_opto_config_t cfg = { + .callbacks = { on_opto_change, on_opto_change, NULL }, + .rs_as_gpio = false, + .debounce_ms = 10U, +}; +bsp_opto_init(&cfg); + +/* в main loop: */ +for (;;) { + bsp_opto_process(); + /* ... */ +} +``` + +Полинг без коллбэков: + +```c +bsp_opto_state_t state = bsp_opto_read(BSP_OPTO_CH_IN1); +``` + +## Совместное использование RS_RX + +Пин GPIO_AD_B1_07 может работать в двух режимах: + +| Режим | BSP-модуль | pin_mux функция | +|----------------|-----------------|-------------------------| +| LPUART3 RX | `bsp_uart_rs` | `BOARD_InitRS_UART()` | +| GPIO input | `bsp_opto` | `BOARD_InitRS_GPIO()` | + +Одновременно использовать оба нельзя. В `firmware_test` режим выбирается +при инициализации в зависимости от конфигурации теста. diff --git a/bsp/opto/include/bsp/opto.h b/bsp/opto/include/bsp/opto.h new file mode 100644 index 0000000..12223e6 --- /dev/null +++ b/bsp/opto/include/bsp/opto.h @@ -0,0 +1,145 @@ +/* + * bsp_opto — оптоизолированные входы + * + * Аппаратура: + * EXT_IN1 GPIO1[22] GPIO_AD_B1_06 active-low (PS2801-4, pull-up 1K к 3V3) + * EXT_IN2 GPIO1[21] GPIO_AD_B1_05 active-low (PS2801-4, pull-up 1K к 3V3) + * RS_RX GPIO1[23] GPIO_AD_B1_07 active-low (PS2801-4, pull-up 1K к 3V3) + * опциональный канал — только при rs_as_gpio == true + * + * Дебаунс: программный, на базе bsp_tick. + * ISR фиксирует timestamp + raw state → bsp_opto_process() из main loop + * подтверждает стабильное состояние и стреляет коллбэком. + * + * Использование: + * bsp_opto_config_t cfg = { + * .callbacks = { my_cb, my_cb, NULL }, + * .rs_as_gpio = false, + * .debounce_ms = 10U, + * }; + * bsp_opto_init(&cfg); + * + * // в main loop: + * bsp_opto_process(); + */ + +#pragma once + +#include "bsp/status.h" + +#include +#include + +/* ── Каналы ──────────────────────────────────────────────────────────────── */ + +typedef enum +{ + BSP_OPTO_CH_IN1 = 0U, /* EXT_IN1 — всегда доступен */ + BSP_OPTO_CH_IN2, /* EXT_IN2 — всегда доступен */ + BSP_OPTO_CH_RS, /* RS_RX — только при rs_as_gpio == true */ + BSP_OPTO_CH_COUNT, +} bsp_opto_ch_t; + +/* ── Состояние канала ────────────────────────────────────────────────────── */ + +typedef enum +{ + BSP_OPTO_STATE_INACTIVE = 0U, /* цепь разомкнута, тока нет */ + BSP_OPTO_STATE_ACTIVE, /* цепь замкнута, ток течёт */ +} bsp_opto_state_t; + +/* ── Фронт прерывания ────────────────────────────────────────────────────── */ + +typedef enum +{ + BSP_OPTO_EDGE_RISING = 0U, + BSP_OPTO_EDGE_FALLING, +} bsp_opto_edge_t; + +/* ── Коллбэк ─────────────────────────────────────────────────────────────── */ + +/** + * @brief Коллбэк изменения состояния канала. + * + * Вызывается из bsp_opto_process() (контекст main loop, не ISR). + * @param ch канал, изменивший состояние + * @param state новое подтверждённое (дебаунсированное) состояние + */ +typedef void (*bsp_opto_callback_t)(bsp_opto_ch_t input_channel, bsp_opto_state_t state); + +/* ── Конфигурация ────────────────────────────────────────────────────────── */ + +typedef struct +{ + /** + * Массив коллбэков, индекс = bsp_opto_ch_t. + * NULL — коллбэк для данного канала не используется. + */ + bsp_opto_callback_t callbacks[BSP_OPTO_CH_COUNT]; + + /** + * Фронт срабатывания прерывания + * + */ + bsp_opto_edge_t edges[BSP_OPTO_CH_COUNT]; + + /** + * true — RS_RX сконфигурировать как GPIO (BSP_OPTO_CH_RS активен). + * false — RS_RX остаётся в состоянии по умолчанию (LPUART3 / AIN). + * BSP_OPTO_CH_RS недоступен, его коллбэк игнорируется. + */ + bool rs_as_gpio; + + /** + * Период дебаунса в миллисекундах. Рекомендуется 5–10 мс. + * 0 — дебаунс отключён (для тестирования). + */ + uint32_t debounce_ms; +} bsp_opto_config_t; + +/* ── API ─────────────────────────────────────────────────────────────────── */ + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * @brief Инициализировать модуль. + * + * Конфигурирует пины (pin mux), настраивает GPIO на вход, + * включает прерывания по обоим фронтам. Для RS_RX вызывает + * BOARD_InitRS_GPIO() если rs_as_gpio == true. + * + * @param p_config указатель на конфигурацию (не NULL) + * @return BSP_OK или BSP_ERR_INVALID_ARG + */ + bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config); + + /** + * @brief Прочитать текущее подтверждённое состояние канала. + * + * Возвращает последнее состояние, зафиксированное после дебаунса. + * Может вызываться из любого контекста. + * + * @param ch номер канала + * @return BSP_OPTO_STATE_INACTIVE / BSP_OPTO_STATE_ACTIVE + */ + bsp_opto_state_t bsp_opto_read(bsp_opto_ch_t input_channel); + + /** + * @brief Обработать отложенные события дебаунса. + * + * Должна вызываться из main loop на каждой итерации. + * НЕ вызывать из ISR. + * + * Для каждого канала с pending-флагом: + * — если прошло >= debounce_ms с последнего фронта, + * перечитывает пин, сравнивает с confirmed_state, + * при изменении вызывает коллбэк и обновляет confirmed_state. + */ + void bsp_opto_process(void); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/bsp/opto/src/opto.c b/bsp/opto/src/opto.c new file mode 100644 index 0000000..7de63b7 --- /dev/null +++ b/bsp/opto/src/opto.c @@ -0,0 +1,214 @@ +/* + * bsp_opto — реализация + */ + +#include "bsp/opto.h" + +#include "board.h" +#include "bsp/tick.h" +#include "fsl_gpio.h" +#include "fsl_iomuxc.h" +#include "pin_mux.h" + +#include +#include + +/* ── Константы пинов ─────────────────────────────────────────────────────── */ + +/* Все три пина — GPIO1, банк [16..31] → GPIO1_Combined_16_31_IRQn */ + +#define OPTO_GPIO_BASE GPIO1 + +#define OPTO_IN1_PIN 22U +#define OPTO_IN2_PIN 21U +#define OPTO_RS_PIN 23U + +/* Active-low: оптопара тянет пин к GND → ACTIVE = LOW = 0 */ +#define OPTO_PIN_TO_STATE(raw) ((raw) == 0U ? BSP_OPTO_STATE_ACTIVE : BSP_OPTO_STATE_INACTIVE) + +/* ── Состояние канала ────────────────────────────────────────────────────── */ + +typedef struct +{ + volatile uint32_t last_edge_ms; /* bsp_tick_get_ms() в момент фронта */ + volatile bool pending; /* фронт зафиксирован, ждём дебаунс */ + volatile uint32_t raw_pin; /* raw состояние пина в момент фронта */ + bsp_opto_state_t confirmed_state; /* последнее подтверждённое состояние */ + uint8_t pin; /* номер пина в GPIO1 */ + bool enabled; /* канал активен */ +} opto_ch_state_t; + +/* ── Модульное состояние ─────────────────────────────────────────────────── */ + +typedef struct +{ + opto_ch_state_t channels[BSP_OPTO_CH_COUNT]; + bsp_opto_callback_t callbacks[BSP_OPTO_CH_COUNT]; + uint32_t debounce_ms; + bool initialised; +} opto_module_t; + +static opto_module_t g_s_opto; + +/* ── Вспомогательные функции ─────────────────────────────────────────────── */ + +static uint32_t read_pin(uint8_t pin) +{ + return GPIO_PinRead(OPTO_GPIO_BASE, pin); +} + +static void enable_irq(uint8_t pin, bsp_opto_edge_t edge) +{ + gpio_interrupt_mode_t mode = + (edge == BSP_OPTO_EDGE_RISING) ? kGPIO_IntRisingEdge : kGPIO_IntFallingEdge; + GPIO_SetPinInterruptConfig(OPTO_GPIO_BASE, pin, mode); + GPIO_EnableInterrupts(OPTO_GPIO_BASE, 1UL << pin); +} + +/* ── ISR ─────────────────────────────────────────────────────────────────── */ + +/* + * Общий обработчик для GPIO1[16..31]. + * Все три канала (пины 21, 22, 23) попадают сюда. + */ +void GPIO1_Combined_16_31_IRQHandler(void) // NOLINT(readability-identifier-naming) +{ + uint32_t flags = GPIO_GetPinsInterruptFlags(OPTO_GPIO_BASE); + + for (bsp_opto_ch_t ch = 0U; ch < BSP_OPTO_CH_COUNT; ch++) + { + opto_ch_state_t *p_ch = &g_s_opto.channels[ch]; + + if (!p_ch->enabled) + { + continue; + } + + uint32_t mask = 1UL << p_ch->pin; + if ((flags & mask) != 0U) + { + p_ch->raw_pin = read_pin(p_ch->pin); + p_ch->last_edge_ms = bsp_tick_get_ms(); + p_ch->pending = true; + + GPIO_ClearPinsInterruptFlags(OPTO_GPIO_BASE, mask); + } + } + + SDK_ISR_EXIT_BARRIER; +} + +/* ── Инициализация ───────────────────────────────────────────────────────── */ + +bsp_status_t bsp_opto_init(const bsp_opto_config_t *p_config) +{ + if (p_config == NULL) + { + return BSP_ERR_PARAM; + } + + /* Сохранить конфигурацию */ + for (bsp_opto_ch_t ch = 0U; ch < BSP_OPTO_CH_COUNT; ch++) + { + g_s_opto.callbacks[ch] = p_config->callbacks[ch]; + } + g_s_opto.debounce_ms = p_config->debounce_ms; + + /* Настройка каналов IN1, IN2 — всегда активны */ + static const uint8_t K_PINS[BSP_OPTO_CH_COUNT] = { + [BSP_OPTO_CH_IN1] = OPTO_IN1_PIN, + [BSP_OPTO_CH_IN2] = OPTO_IN2_PIN, + [BSP_OPTO_CH_RS] = OPTO_RS_PIN, + }; + + gpio_pin_config_t gpio_in_cfg = { + .direction = kGPIO_DigitalInput, + .outputLogic = 0U, + .interruptMode = kGPIO_NoIntmode, + }; + + for (bsp_opto_ch_t ch = 0U; ch < BSP_OPTO_CH_COUNT; ch++) + { + opto_ch_state_t *p_ch = &g_s_opto.channels[ch]; + + if (ch == BSP_OPTO_CH_RS && !p_config->rs_as_gpio) + { + p_ch->enabled = false; + continue; + } + + p_ch->pin = K_PINS[ch]; + p_ch->enabled = true; + p_ch->pending = false; + + /* Начальное состояние — читаем пин до включения прерываний */ + if (ch == BSP_OPTO_CH_RS) + { + BOARD_InitRS_GPIO(); + } + + GPIO_PinInit(OPTO_GPIO_BASE, p_ch->pin, &gpio_in_cfg); + p_ch->confirmed_state = OPTO_PIN_TO_STATE(read_pin(p_ch->pin)); + + enable_irq(p_ch->pin, p_config->edges[ch]); + } + + /* Включить IRQ GPIO1_Combined_16_31 */ + EnableIRQ(GPIO1_Combined_16_31_IRQn); + + g_s_opto.initialised = true; + return BSP_OK; +} + +/* ── Чтение состояния ────────────────────────────────────────────────────── */ + +bsp_opto_state_t bsp_opto_read(bsp_opto_ch_t input_channel) +{ + if (input_channel >= BSP_OPTO_CH_COUNT || !g_s_opto.channels[input_channel].enabled) + { + return BSP_OPTO_STATE_INACTIVE; + } + return g_s_opto.channels[input_channel].confirmed_state; +} + +/* ── Обработка дебаунса ──────────────────────────────────────────────────── */ + +void bsp_opto_process(void) +{ + if (!g_s_opto.initialised) + { + return; + } + + uint32_t now = bsp_tick_get_ms(); + + for (bsp_opto_ch_t ch = 0U; ch < BSP_OPTO_CH_COUNT; ch++) + { + opto_ch_state_t *p_ch = &g_s_opto.channels[ch]; + + if (!p_ch->enabled || !p_ch->pending) + { + continue; + } + + uint32_t elapsed = now - p_ch->last_edge_ms; + if (elapsed < g_s_opto.debounce_ms) + { + continue; + } + + /* Дебаунс истёк — перечитать пин и подтвердить состояние */ + bsp_opto_state_t new_state = OPTO_PIN_TO_STATE(read_pin(p_ch->pin)); + p_ch->pending = false; + + if (new_state != p_ch->confirmed_state) + { + p_ch->confirmed_state = new_state; + + if (g_s_opto.callbacks[ch] != NULL) + { + g_s_opto.callbacks[ch](ch, new_state); + } + } + } +} \ No newline at end of file diff --git a/bsp/tick/src/tick.c b/bsp/tick/src/tick.c index 9490d89..86ed4a8 100644 --- a/bsp/tick/src/tick.c +++ b/bsp/tick/src/tick.c @@ -26,26 +26,27 @@ __attribute__((weak)) void bsp_systick_hook(void) #ifndef BSP_TICK_FREERTOS_MODE -#include "fsl_common.h" /* SystemCoreClock, SysTick_Config */ +#include "fsl_common.h" -static volatile uint32_t s_tick_ms = 0U; +static volatile uint32_t g_s_tick_ms = 0U; +static const uint16_t MS_DIVIDER = 1000; // Переполнение раз в 1 мс void bsp_tick_init(void) { - SysTick_Config(SystemCoreClock / 1000U); + SysTick_Config(SystemCoreClock / MS_DIVIDER); } void bsp_tick_inc(void) { - s_tick_ms++; + g_s_tick_ms++; } uint32_t bsp_tick_get_ms(void) { - return s_tick_ms; + return g_s_tick_ms; } -void SysTick_Handler(void) +void SysTick_Handler(void) // NOLINT(readability-identifier-naming) { bsp_tick_inc(); bsp_systick_hook(); diff --git a/tests/HOST_CREATE_TEST.md b/tests/HOST_CREATE_TEST.md index 36cf139..0c78580 100644 --- a/tests/HOST_CREATE_TEST.md +++ b/tests/HOST_CREATE_TEST.md @@ -2,7 +2,7 @@ ## Обзор стека -``` +```bash devcontainer ───────────────────────────────────────────────────────────────── tests/host//test_.c ← тест (Unity + опционально fff) @@ -366,11 +366,11 @@ just build::test-host ## Чеклист -``` +```bash [ ] Определена категория (A или B) [ ] tests/host//test_.c — тест с main(), setUp(), tearDown() [ ] tests/host/mocks/fsl_.h — stub (только категория B, если нет) [ ] tests/host/CMakeLists.txt — add_host_test(NAME test_ ...) [ ] CMakePresets.json — добавить test_ в host-debug-build [ ] just build::test-host — зелёный прогон -``` \ No newline at end of file +``` diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index 6a89711..09571a3 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -94,8 +94,7 @@ add_host_test( ) # ----------------------------------------------------------------------------- -# Тест кода, использующего bsp_uart_host через мок (замените SOURCES на свой -# реальный тест и код под тестом) +# Тест кода, использующего bsp_uart_host через мок # ----------------------------------------------------------------------------- add_host_test( NAME @@ -112,3 +111,18 @@ add_host_test( # ${CMAKE_SOURCE_DIR}/bsp/protocol/include # protocol.h MOCKS ${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks/uart_host_mock.c) + +# ----------------------------------------------------------------------------- +# Тест кода, использующего bsp_opto через мок +# ----------------------------------------------------------------------------- +add_host_test( + NAME + test_bsp_opto + SOURCES + opto/test_bsp_opto.c + ${PROJECT_SOURCE_DIR}/bsp/opto/src/opto.c + INCLUDES + ${PROJECT_SOURCE_DIR}/bsp/opto/include + ${PROJECT_SOURCE_DIR}/bsp/common/include + MOCKS + ${BSP_MOCKS_DIR}) diff --git a/tests/host/mocks/board.h b/tests/host/mocks/board.h index 37f154d..32a58b0 100644 --- a/tests/host/mocks/board.h +++ b/tests/host/mocks/board.h @@ -1,2 +1,13 @@ #pragma once -/* Stub board.h для host-тестов — led.c включает его транзитивно через bsp_board */ \ No newline at end of file + +/** + * Stub board.h для host-тестов. + * Содержит только сигнатуры используемые в bsp-модулях. + * fff предоставляет реализации в тест-файлах где нужно. + */ + +void BOARD_InitBootPins(void); +void BOARD_InitPins(void); +void BOARD_InitRS_UART(void); +void BOARD_InitRS_GPIO(void); +void board_hw_init(void); \ No newline at end of file diff --git a/tests/host/mocks/bsp/tick.h b/tests/host/mocks/bsp/tick.h new file mode 100644 index 0000000..c30fbcb --- /dev/null +++ b/tests/host/mocks/bsp/tick.h @@ -0,0 +1,11 @@ +#pragma once + +/** + * Stub bsp/tick.h для host-тестов. + * bsp_tick_get_ms() мокается через fff в тест-файлах где нужен контроль времени. + */ + +#include + +uint32_t bsp_tick_get_ms(void); +void bsp_delay(uint32_t ms); \ No newline at end of file diff --git a/tests/host/mocks/fsl_gpio.h b/tests/host/mocks/fsl_gpio.h index 453d687..ccda5c0 100644 --- a/tests/host/mocks/fsl_gpio.h +++ b/tests/host/mocks/fsl_gpio.h @@ -2,32 +2,63 @@ /** * Stub fsl_gpio.h для host-тестов. - * Содержит только типы и сигнатуры используемые в bsp/led/src/led.c. - * fff предоставляет реализации через FAKE_VOID_FUNC. + * Содержит типы и сигнатуры используемые в: + * bsp/led/src/led.c + * bsp/opto/src/opto.c + * + * fff предоставляет реализации через FAKE_VOID_FUNC / FAKE_VALUE_FUNC. */ #include -/* GPIO peripheral base pointer type */ +/* ── GPIO peripheral ─────────────────────────────────────────────────────── */ + typedef struct { uint32_t reserved[64]; } GPIO_Type; -/* Pin direction */ +/* + * Stub-экземпляры и макросы GPIO1/2/3. + * static — каждая translation unit получает свою копию (нормально для тестов). + * #ifndef защита — на случай если pin_mux.h включён раньше. + */ +#ifndef GPIO1 +static GPIO_Type stub_GPIO1; +static GPIO_Type stub_GPIO2; +static GPIO_Type stub_GPIO3; + +#define GPIO1 (&stub_GPIO1) +#define GPIO2 (&stub_GPIO2) +#define GPIO3 (&stub_GPIO3) +#endif + +/* ── Enums ───────────────────────────────────────────────────────────────── */ + typedef enum { kGPIO_DigitalInput = 0U, kGPIO_DigitalOutput = 1U, } gpio_pin_direction_t; -/* Interrupt mode — не используется в LED, но нужен для компиляции */ typedef enum { - kGPIO_NoIntmode = 0U, + kGPIO_NoIntmode = 0U, + kGPIO_IntLowLevel = 1U, + kGPIO_IntHighLevel = 2U, + kGPIO_IntRisingEdge = 3U, + kGPIO_IntFallingEdge = 4U, + kGPIO_IntRisingOrFallingEdge = 5U, } gpio_interrupt_mode_t; -/* Pin config struct */ +/* IRQ-номера — только те что нужны для opto */ +typedef enum +{ + GPIO1_Combined_16_31_IRQn = 100, +} IRQn_Type; + +/* ── Pin config ──────────────────────────────────────────────────────────── */ + typedef struct { gpio_pin_direction_t direction; @@ -35,6 +66,21 @@ typedef struct gpio_interrupt_mode_t interruptMode; } gpio_pin_config_t; -/* Functions — реализуются через fff в тест-файле */ +/* ── CMSIS / SDK макросы ─────────────────────────────────────────────────── */ + +/* На хосте — no-op */ +#define SDK_ISR_EXIT_BARRIER ((void) 0) + +/* ── Function signatures — реализуются через fff ─────────────────────────── */ + 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); \ No newline at end of file +void GPIO_PinWrite(GPIO_Type *base, uint32_t pin, uint8_t output); +uint32_t GPIO_PinRead(GPIO_Type *base, uint32_t pin); + +void GPIO_SetPinInterruptConfig(GPIO_Type *base, uint32_t pin, + gpio_interrupt_mode_t pinInterruptMode); +void GPIO_EnableInterrupts(GPIO_Type *base, uint32_t mask); +uint32_t GPIO_GetPinsInterruptFlags(GPIO_Type *base); +void GPIO_ClearPinsInterruptFlags(GPIO_Type *base, uint32_t mask); + +void EnableIRQ(IRQn_Type irq); \ No newline at end of file diff --git a/tests/host/mocks/fsl_iomuxc.h b/tests/host/mocks/fsl_iomuxc.h new file mode 100644 index 0000000..53de2b8 --- /dev/null +++ b/tests/host/mocks/fsl_iomuxc.h @@ -0,0 +1,8 @@ +#pragma once + +/** + * Stub fsl_iomuxc.h для host-тестов. + * В реальном SDK содержит только inline-функции для настройки pin mux. + * В opto.c используется как транзитивный include — прямых вызовов нет, + * поэтому stub пустой. + */ \ No newline at end of file diff --git a/tests/host/mocks/pin_mux.h b/tests/host/mocks/pin_mux.h index 43bc046..f61f861 100644 --- a/tests/host/mocks/pin_mux.h +++ b/tests/host/mocks/pin_mux.h @@ -3,20 +3,17 @@ /** * Stub pin_mux.h для host-тестов. * Макросы пинов зеркалят реальные значения из generated/pin_mux.h. - * GPIO-инстансы — статические объекты-заглушки, адреса уникальны. + * GPIO-инстансы приходят из fsl_gpio.h (stub_GPIO1/2/3). */ #include "fsl_gpio.h" -/* Заглушки GPIO-периферии */ -static GPIO_Type stub_GPIO3; - /* UserLed1 → GPIO3 pin 3 (LED_HEARTBEAT) */ -#define BOARD_INITPINS_UserLed1_GPIO (&stub_GPIO3) +#define BOARD_INITPINS_UserLed1_GPIO GPIO3 #define BOARD_INITPINS_UserLed1_GPIO_PIN 3U #define BOARD_INITPINS_UserLed1_INIT_GPIO_VALUE 1U /* UserLed2 → GPIO3 pin 4 (LED_APP) */ -#define BOARD_INITPINS_UserLed2_GPIO (&stub_GPIO3) +#define BOARD_INITPINS_UserLed2_GPIO GPIO3 #define BOARD_INITPINS_UserLed2_GPIO_PIN 4U #define BOARD_INITPINS_UserLed2_INIT_GPIO_VALUE 1U \ No newline at end of file diff --git a/tests/host/opto/test_bsp_opto.c b/tests/host/opto/test_bsp_opto.c new file mode 100644 index 0000000..51bda24 --- /dev/null +++ b/tests/host/opto/test_bsp_opto.c @@ -0,0 +1,492 @@ +/* + * test_bsp_opto.c — host-тест bsp_opto + * + * Категория B: opto.c вызывает fsl_gpio.h и bsp_tick. + * Все зависимости заменены fff-фейками. + * + * Топология пинов (из opto.c): + * BSP_OPTO_CH_IN1 → GPIO1 pin 22 + * BSP_OPTO_CH_IN2 → GPIO1 pin 21 + * BSP_OPTO_CH_RS → GPIO1 pin 23 + */ + +#include "fff.h" +#include "unity.h" + +DEFINE_FFF_GLOBALS; + +/* ── 1. Стабы SDK (до тестируемого модуля) ───────────────────────────────── */ + +#include "fsl_gpio.h" + +FAKE_VOID_FUNC(GPIO_PinInit, GPIO_Type *, uint32_t, const gpio_pin_config_t *); +FAKE_VOID_FUNC(GPIO_PinWrite, GPIO_Type *, uint32_t, uint8_t); +FAKE_VALUE_FUNC(uint32_t, GPIO_PinRead, GPIO_Type *, uint32_t); + +FAKE_VOID_FUNC(GPIO_SetPinInterruptConfig, GPIO_Type *, uint32_t, gpio_interrupt_mode_t); +FAKE_VOID_FUNC(GPIO_EnableInterrupts, GPIO_Type *, uint32_t); +FAKE_VALUE_FUNC(uint32_t, GPIO_GetPinsInterruptFlags, GPIO_Type *); +FAKE_VOID_FUNC(GPIO_ClearPinsInterruptFlags, GPIO_Type *, uint32_t); + +FAKE_VOID_FUNC(EnableIRQ, IRQn_Type); + +/* ── 2. Стабы BSP ─────────────────────────────────────────────────────────── */ + +#include "board.h" +FAKE_VOID_FUNC(BOARD_InitRS_GPIO); + +#include "bsp/tick.h" +FAKE_VALUE_FUNC(uint32_t, bsp_tick_get_ms); + +/* ── 3. Тестируемый модуль — всегда последним ────────────────────────────── */ + +#include "bsp/opto.h" + +/* ── Вспомогательное ─────────────────────────────────────────────────────── */ + +#define OPTO_IN1_PIN 22U +#define OPTO_IN2_PIN 21U +#define OPTO_RS_PIN 23U + +#define DEBOUNCE_MS 10U + +/* Capture конфигурации GPIO_PinInit (та же техника что в test_bsp_led.c) */ +#define MAX_INIT_CALLS 3U +static gpio_pin_config_t s_captured_cfg[MAX_INIT_CALLS]; +static uint32_t s_captured_pin[MAX_INIT_CALLS]; +static uint32_t s_capture_idx; + +static void GPIO_PinInit_capture(GPIO_Type *base, uint32_t pin, const gpio_pin_config_t *p_cfg) +{ + (void) base; + if (s_capture_idx < MAX_INIT_CALLS) + { + s_captured_pin[s_capture_idx] = pin; + s_captured_cfg[s_capture_idx] = *p_cfg; + s_capture_idx++; + } +} + +/* Capture SetPinInterruptConfig */ +#define MAX_IRQ_CONFIG_CALLS 3U +static gpio_interrupt_mode_t s_captured_irq_mode[MAX_IRQ_CONFIG_CALLS]; +static uint32_t s_captured_irq_pin[MAX_IRQ_CONFIG_CALLS]; +static uint32_t s_irq_config_idx; + +static void GPIO_SetPinInterruptConfig_capture(GPIO_Type *base, uint32_t pin, + gpio_interrupt_mode_t mode) +{ + (void) base; + if (s_irq_config_idx < MAX_IRQ_CONFIG_CALLS) + { + s_captured_irq_pin[s_irq_config_idx] = pin; + s_captured_irq_mode[s_irq_config_idx] = mode; + s_irq_config_idx++; + } +} + +/* Коллбэк-фиксатор */ +static bsp_opto_ch_t s_cb_ch; +static bsp_opto_state_t s_cb_state; +static uint32_t s_cb_count; + +static void test_callback(bsp_opto_ch_t ch, bsp_opto_state_t state) +{ + s_cb_ch = ch; + s_cb_state = state; + s_cb_count++; +} + +/* Конфигурация по умолчанию (rs_as_gpio = false, 2 канала) */ +static bsp_opto_config_t make_default_cfg(void) +{ + bsp_opto_config_t cfg = { + .callbacks = { test_callback, test_callback, NULL }, + .edges = { + [BSP_OPTO_CH_IN1] = BSP_OPTO_EDGE_RISING, + [BSP_OPTO_CH_IN2] = BSP_OPTO_EDGE_RISING, + [BSP_OPTO_CH_RS] = BSP_OPTO_EDGE_RISING, + }, + .rs_as_gpio = false, + .debounce_ms = DEBOUNCE_MS, + }; + return cfg; +} + +/* Forward declaration ISR — определён в opto.c */ +void GPIO1_Combined_16_31_IRQHandler(void); + +/* Симулировать ISR — дёргаем обработчик напрямую как из железа */ +static void simulate_isr(uint8_t pin, uint32_t pin_level) +{ + GPIO_GetPinsInterruptFlags_fake.return_val = (1UL << pin); + GPIO_PinRead_fake.return_val = pin_level; + GPIO1_Combined_16_31_IRQHandler(); +} + +/* ── setUp / tearDown ────────────────────────────────────────────────────── */ + +void setUp(void) +{ + RESET_FAKE(GPIO_PinInit); + RESET_FAKE(GPIO_PinWrite); + RESET_FAKE(GPIO_PinRead); + RESET_FAKE(GPIO_SetPinInterruptConfig); + RESET_FAKE(GPIO_EnableInterrupts); + RESET_FAKE(GPIO_GetPinsInterruptFlags); + RESET_FAKE(GPIO_ClearPinsInterruptFlags); + RESET_FAKE(EnableIRQ); + RESET_FAKE(BOARD_InitRS_GPIO); + RESET_FAKE(bsp_tick_get_ms); + FFF_RESET_HISTORY(); + + s_capture_idx = 0U; + s_irq_config_idx = 0U; + s_cb_count = 0U; + + GPIO_PinInit_fake.custom_fake = GPIO_PinInit_capture; + GPIO_SetPinInterruptConfig_fake.custom_fake = GPIO_SetPinInterruptConfig_capture; + + /* По умолчанию пины INACTIVE (active-low → raw = 1) */ + GPIO_PinRead_fake.return_val = 1U; +} + +void tearDown(void) +{ +} + +/* ── Тесты: bsp_opto_init ────────────────────────────────────────────────── */ + +void test_init_returns_ok(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + TEST_ASSERT_EQUAL(BSP_OK, bsp_opto_init(&cfg)); +} + +void test_init_null_config_returns_err(void) +{ + TEST_ASSERT_EQUAL(BSP_ERR_PARAM, bsp_opto_init(NULL)); +} + +void test_init_two_channels_when_rs_as_gpio_false(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + /* IN1 + IN2, RS пропущен */ + TEST_ASSERT_EQUAL(2U, GPIO_PinInit_fake.call_count); +} + +void test_init_three_channels_when_rs_as_gpio_true(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + cfg.rs_as_gpio = true; + cfg.callbacks[BSP_OPTO_CH_RS] = test_callback; + bsp_opto_init(&cfg); + TEST_ASSERT_EQUAL(3U, GPIO_PinInit_fake.call_count); +} + +void test_init_all_pins_configured_as_input(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + TEST_ASSERT_EQUAL(kGPIO_DigitalInput, s_captured_cfg[0].direction); + TEST_ASSERT_EQUAL(kGPIO_DigitalInput, s_captured_cfg[1].direction); +} + +void test_init_correct_pins_configured(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + /* Порядок в реестре: IN1=22, IN2=21 */ + TEST_ASSERT_EQUAL(OPTO_IN1_PIN, s_captured_pin[0]); + TEST_ASSERT_EQUAL(OPTO_IN2_PIN, s_captured_pin[1]); +} + +void test_init_irq_enabled_for_each_channel(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + TEST_ASSERT_EQUAL(2U, GPIO_EnableInterrupts_fake.call_count); +} + +void test_init_global_irq_enabled(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + TEST_ASSERT_EQUAL(1U, EnableIRQ_fake.call_count); + TEST_ASSERT_EQUAL(GPIO1_Combined_16_31_IRQn, EnableIRQ_fake.arg0_val); +} + +void test_init_rs_as_gpio_calls_board_init(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + cfg.rs_as_gpio = true; + bsp_opto_init(&cfg); + TEST_ASSERT_EQUAL(1U, BOARD_InitRS_GPIO_fake.call_count); +} + +void test_init_rs_as_gpio_false_no_board_init(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + TEST_ASSERT_EQUAL(0U, BOARD_InitRS_GPIO_fake.call_count); +} + +/* ── Тесты: edge config ──────────────────────────────────────────────────── */ + +void test_init_rising_edge_sets_rising_irq_mode(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + cfg.edges[BSP_OPTO_CH_IN1] = BSP_OPTO_EDGE_RISING; + bsp_opto_init(&cfg); + + /* Найти запись для пина IN1 */ + uint32_t idx = 0U; + for (uint32_t i = 0U; i < s_irq_config_idx; i++) + { + if (s_captured_irq_pin[i] == OPTO_IN1_PIN) + { + idx = i; + break; + } + } + TEST_ASSERT_EQUAL(kGPIO_IntRisingEdge, s_captured_irq_mode[idx]); +} + +void test_init_falling_edge_sets_falling_irq_mode(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + cfg.edges[BSP_OPTO_CH_IN1] = BSP_OPTO_EDGE_FALLING; + bsp_opto_init(&cfg); + + uint32_t idx = 0U; + for (uint32_t i = 0U; i < s_irq_config_idx; i++) + { + if (s_captured_irq_pin[i] == OPTO_IN1_PIN) + { + idx = i; + break; + } + } + TEST_ASSERT_EQUAL(kGPIO_IntFallingEdge, s_captured_irq_mode[idx]); +} + +/* ── Тесты: bsp_opto_read — начальное состояние ─────────────────────────── */ + +void test_read_initial_inactive_when_pin_high(void) +{ + /* active-low: raw=1 → INACTIVE */ + GPIO_PinRead_fake.return_val = 1U; + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + + TEST_ASSERT_EQUAL(BSP_OPTO_STATE_INACTIVE, bsp_opto_read(BSP_OPTO_CH_IN1)); + TEST_ASSERT_EQUAL(BSP_OPTO_STATE_INACTIVE, bsp_opto_read(BSP_OPTO_CH_IN2)); +} + +void test_read_initial_active_when_pin_low(void) +{ + /* active-low: raw=0 → ACTIVE */ + GPIO_PinRead_fake.return_val = 0U; + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + + TEST_ASSERT_EQUAL(BSP_OPTO_STATE_ACTIVE, bsp_opto_read(BSP_OPTO_CH_IN1)); +} + +void test_read_disabled_rs_channel_returns_inactive(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); /* rs_as_gpio = false */ + bsp_opto_init(&cfg); + TEST_ASSERT_EQUAL(BSP_OPTO_STATE_INACTIVE, bsp_opto_read(BSP_OPTO_CH_RS)); +} + +void test_read_invalid_channel_returns_inactive(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + TEST_ASSERT_EQUAL(BSP_OPTO_STATE_INACTIVE, bsp_opto_read((bsp_opto_ch_t) BSP_OPTO_CH_COUNT)); +} + +/* ── Тесты: bsp_opto_process — дебаунс ──────────────────────────────────── */ + +void test_process_before_debounce_no_callback(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + + /* ISR фиксирует фронт в момент T=100 */ + bsp_tick_get_ms_fake.return_val = 100U; + simulate_isr(OPTO_IN1_PIN, 0U); /* pin → ACTIVE */ + + /* process в T=105: прошло 5ms < debounce_ms(10) */ + bsp_tick_get_ms_fake.return_val = 105U; + GPIO_PinRead_fake.return_val = 0U; + bsp_opto_process(); + + TEST_ASSERT_EQUAL(0U, s_cb_count); +} + +void test_process_after_debounce_fires_callback(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + + bsp_tick_get_ms_fake.return_val = 100U; + simulate_isr(OPTO_IN1_PIN, 0U); /* pin → ACTIVE */ + + /* process в T=112: прошло 12ms >= debounce_ms(10) */ + bsp_tick_get_ms_fake.return_val = 112U; + GPIO_PinRead_fake.return_val = 0U; + bsp_opto_process(); + + TEST_ASSERT_EQUAL(1U, s_cb_count); +} + +void test_process_callback_receives_correct_channel(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + + bsp_tick_get_ms_fake.return_val = 0U; + simulate_isr(OPTO_IN1_PIN, 0U); + + bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U; + GPIO_PinRead_fake.return_val = 0U; + bsp_opto_process(); + + TEST_ASSERT_EQUAL(BSP_OPTO_CH_IN1, s_cb_ch); +} + +void test_process_callback_receives_active_state(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + + bsp_tick_get_ms_fake.return_val = 0U; + simulate_isr(OPTO_IN1_PIN, 0U); /* raw=0 → ACTIVE */ + + bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U; + GPIO_PinRead_fake.return_val = 0U; + bsp_opto_process(); + + TEST_ASSERT_EQUAL(BSP_OPTO_STATE_ACTIVE, s_cb_state); +} + +void test_process_callback_receives_inactive_state(void) +{ + /* Начинаем с ACTIVE — init с pin=0 */ + GPIO_PinRead_fake.return_val = 0U; + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + + /* Фронт: pin → INACTIVE (raw=1) */ + bsp_tick_get_ms_fake.return_val = 0U; + simulate_isr(OPTO_IN1_PIN, 1U); + + bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U; + GPIO_PinRead_fake.return_val = 1U; + bsp_opto_process(); + + TEST_ASSERT_EQUAL(BSP_OPTO_STATE_INACTIVE, s_cb_state); +} + +void test_process_no_callback_if_state_unchanged(void) +{ + /* Init с pin=0 → ACTIVE */ + GPIO_PinRead_fake.return_val = 0U; + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + + /* ISR срабатывает, но пин при перечитке всё ещё ACTIVE */ + bsp_tick_get_ms_fake.return_val = 0U; + simulate_isr(OPTO_IN1_PIN, 0U); + + bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U; + GPIO_PinRead_fake.return_val = 0U; /* состояние не изменилось */ + bsp_opto_process(); + + TEST_ASSERT_EQUAL(0U, s_cb_count); +} + +void test_process_pending_cleared_after_debounce(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + + bsp_tick_get_ms_fake.return_val = 0U; + simulate_isr(OPTO_IN1_PIN, 0U); + + bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U; + GPIO_PinRead_fake.return_val = 0U; + bsp_opto_process(); + + /* Второй вызов process без нового ISR — коллбэк не должен стрелять снова */ + bsp_opto_process(); + + TEST_ASSERT_EQUAL(1U, s_cb_count); +} + +/* ── Тесты: независимость каналов ───────────────────────────────────────── */ + +void test_channels_are_independent(void) +{ + bsp_opto_config_t cfg = make_default_cfg(); + bsp_opto_init(&cfg); + + /* ISR только для IN1 */ + bsp_tick_get_ms_fake.return_val = 0U; + simulate_isr(OPTO_IN1_PIN, 0U); + + bsp_tick_get_ms_fake.return_val = DEBOUNCE_MS + 1U; + GPIO_PinRead_fake.return_val = 0U; + bsp_opto_process(); + + /* IN1 изменился, IN2 — нет */ + TEST_ASSERT_EQUAL(BSP_OPTO_STATE_ACTIVE, bsp_opto_read(BSP_OPTO_CH_IN1)); + TEST_ASSERT_EQUAL(BSP_OPTO_STATE_INACTIVE, bsp_opto_read(BSP_OPTO_CH_IN2)); + TEST_ASSERT_EQUAL(1U, s_cb_count); /* ровно один коллбэк */ +} + +/* ── main ────────────────────────────────────────────────────────────────── */ + +int main(void) +{ + UNITY_BEGIN(); + + /* init */ + RUN_TEST(test_init_returns_ok); + RUN_TEST(test_init_null_config_returns_err); + RUN_TEST(test_init_two_channels_when_rs_as_gpio_false); + RUN_TEST(test_init_three_channels_when_rs_as_gpio_true); + RUN_TEST(test_init_all_pins_configured_as_input); + RUN_TEST(test_init_correct_pins_configured); + RUN_TEST(test_init_irq_enabled_for_each_channel); + RUN_TEST(test_init_global_irq_enabled); + RUN_TEST(test_init_rs_as_gpio_calls_board_init); + RUN_TEST(test_init_rs_as_gpio_false_no_board_init); + + /* edge config */ + RUN_TEST(test_init_rising_edge_sets_rising_irq_mode); + RUN_TEST(test_init_falling_edge_sets_falling_irq_mode); + + /* read — начальное состояние */ + RUN_TEST(test_read_initial_inactive_when_pin_high); + RUN_TEST(test_read_initial_active_when_pin_low); + RUN_TEST(test_read_disabled_rs_channel_returns_inactive); + RUN_TEST(test_read_invalid_channel_returns_inactive); + + /* process — дебаунс */ + RUN_TEST(test_process_before_debounce_no_callback); + RUN_TEST(test_process_after_debounce_fires_callback); + RUN_TEST(test_process_callback_receives_correct_channel); + RUN_TEST(test_process_callback_receives_active_state); + RUN_TEST(test_process_callback_receives_inactive_state); + RUN_TEST(test_process_no_callback_if_state_unchanged); + RUN_TEST(test_process_pending_cleared_after_debounce); + + /* независимость каналов */ + RUN_TEST(test_channels_are_independent); + + return UNITY_END(); +} \ No newline at end of file