- написан и покрыт хост-тестами модуль bsp_opto (оптовоходы RS, IN1, IN2)
- TODO: написал первый HIL тест с m5stamplc
This commit is contained in:
Dmitry Akimov 2026-03-23 13:58:09 +03:00
parent c4feaa8f45
commit 6673bf5da6
17 changed files with 1049 additions and 29 deletions

View file

@ -10,6 +10,7 @@ Checks: >
hicpp-signed-bitwise, hicpp-signed-bitwise,
performance-*, performance-*,
portability-*, portability-*,
-performance-no-int-to-ptr,
readability-braces-around-statements, readability-braces-around-statements,
readability-container-size-empty, readability-container-size-empty,
readability-function-size, readability-function-size,

View file

@ -65,6 +65,7 @@ Diagnostics:
- readability-* - readability-*
- portability-* - portability-*
Remove: Remove:
- performance-no-int-to-ptr
- readability-magic-numbers - readability-magic-numbers
- readability-isolate-declaration - readability-isolate-declaration
- bugprone-easily-swappable-parameters - bugprone-easily-swappable-parameters

View file

@ -141,6 +141,7 @@
"targets": [ "targets": [
"test_bsp_led", "test_bsp_led",
"test_log", "test_log",
"test_bsp_opto",
"test_ring_buffer", "test_ring_buffer",
"test_timeout_pattern", "test_timeout_pattern",
"uart_host_mock_example" "uart_host_mock_example"
@ -153,6 +154,7 @@
"targets": [ "targets": [
"test_bsp_led", "test_bsp_led",
"test_log", "test_log",
"test_bsp_opto",
"test_ring_buffer", "test_ring_buffer",
"test_timeout_pattern", "test_timeout_pattern",
"uart_host_mock_example" "uart_host_mock_example"

View file

@ -3,7 +3,7 @@ if(BUILD_TESTS_HOST)
endif() endif()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# bsp_board — стартап, сгенерированные файлы Config Tools, инициализация платы # bsp_board — генерированные файлы Config Tools, инициализация платы
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
add_library(bsp_board STATIC generated/board.c generated/pin_mux.c) add_library(bsp_board STATIC generated/board.c generated/pin_mux.c)
@ -12,7 +12,7 @@ add_subdirectory(common)
add_subdirectory(led) add_subdirectory(led)
add_subdirectory(tick) add_subdirectory(tick)
add_subdirectory(uart_host) add_subdirectory(uart_host)
add_subdirectory(opto)
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки # Подавляем предупреждения при компиляции собственных .c файлов библиотеки
target_compile_options(bsp_board PRIVATE -w) target_compile_options(bsp_board PRIVATE -w)

8
bsp/opto/CMakeLists.txt Normal file
View file

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

69
bsp/opto/README.md Normal file
View file

@ -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` режим выбирается
при инициализации в зависимости от конфигурации теста.

145
bsp/opto/include/bsp/opto.h Normal file
View file

@ -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 <stdbool.h>
#include <stdint.h>
/* ── Каналы ──────────────────────────────────────────────────────────────── */
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;
/**
* Период дебаунса в миллисекундах. Рекомендуется 510 мс.
* 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

214
bsp/opto/src/opto.c Normal file
View file

@ -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 <stddef.h>
#include <stdint.h>
/* ── Константы пинов ─────────────────────────────────────────────────────── */
/* Все три пина — 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);
}
}
}
}

View file

@ -26,26 +26,27 @@ __attribute__((weak)) void bsp_systick_hook(void)
#ifndef BSP_TICK_FREERTOS_MODE #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) void bsp_tick_init(void)
{ {
SysTick_Config(SystemCoreClock / 1000U); SysTick_Config(SystemCoreClock / MS_DIVIDER);
} }
void bsp_tick_inc(void) void bsp_tick_inc(void)
{ {
s_tick_ms++; g_s_tick_ms++;
} }
uint32_t bsp_tick_get_ms(void) 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_tick_inc();
bsp_systick_hook(); bsp_systick_hook();

View file

@ -2,7 +2,7 @@
## Обзор стека ## Обзор стека
``` ```bash
devcontainer devcontainer
───────────────────────────────────────────────────────────────── ─────────────────────────────────────────────────────────────────
tests/host/<n>/test_<n>.c ← тест (Unity + опционально fff) tests/host/<n>/test_<n>.c ← тест (Unity + опционально fff)
@ -366,11 +366,11 @@ just build::test-host
## Чеклист ## Чеклист
``` ```bash
[ ] Определена категория (A или B) [ ] Определена категория (A или B)
[ ] tests/host/<n>/test_<n>.c — тест с main(), setUp(), tearDown() [ ] tests/host/<n>/test_<n>.c — тест с main(), setUp(), tearDown()
[ ] tests/host/mocks/fsl_<driver>.h — stub (только категория B, если нет) [ ] tests/host/mocks/fsl_<driver>.h — stub (только категория B, если нет)
[ ] tests/host/CMakeLists.txt — add_host_test(NAME test_<n> ...) [ ] tests/host/CMakeLists.txt — add_host_test(NAME test_<n> ...)
[ ] CMakePresets.json — добавить test_<n> в host-debug-build [ ] CMakePresets.json — добавить test_<n> в host-debug-build
[ ] just build::test-host — зелёный прогон [ ] just build::test-host — зелёный прогон
``` ```

View file

@ -94,8 +94,7 @@ add_host_test(
) )
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Тест кода, использующего bsp_uart_host через мок (замените SOURCES на свой # Тест кода, использующего bsp_uart_host через мок
# реальный тест и код под тестом)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
add_host_test( add_host_test(
NAME NAME
@ -112,3 +111,18 @@ add_host_test(
# ${CMAKE_SOURCE_DIR}/bsp/protocol/include # protocol.h # ${CMAKE_SOURCE_DIR}/bsp/protocol/include # protocol.h
MOCKS MOCKS
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks/uart_host_mock.c) ${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})

View file

@ -1,2 +1,13 @@
#pragma once #pragma once
/* Stub board.h для host-тестов — led.c включает его транзитивно через bsp_board */
/**
* 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);

View file

@ -0,0 +1,11 @@
#pragma once
/**
* Stub bsp/tick.h для host-тестов.
* bsp_tick_get_ms() мокается через fff в тест-файлах где нужен контроль времени.
*/
#include <stdint.h>
uint32_t bsp_tick_get_ms(void);
void bsp_delay(uint32_t ms);

View file

@ -2,32 +2,63 @@
/** /**
* Stub fsl_gpio.h для host-тестов. * 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 <stdint.h> #include <stdint.h>
/* GPIO peripheral base pointer type */ /* ── GPIO peripheral ─────────────────────────────────────────────────────── */
typedef struct typedef struct
{ {
uint32_t reserved[64]; uint32_t reserved[64];
} GPIO_Type; } 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 typedef enum
{ {
kGPIO_DigitalInput = 0U, kGPIO_DigitalInput = 0U,
kGPIO_DigitalOutput = 1U, kGPIO_DigitalOutput = 1U,
} gpio_pin_direction_t; } gpio_pin_direction_t;
/* Interrupt mode — не используется в LED, но нужен для компиляции */
typedef enum 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; } gpio_interrupt_mode_t;
/* Pin config struct */ /* IRQ-номера — только те что нужны для opto */
typedef enum
{
GPIO1_Combined_16_31_IRQn = 100,
} IRQn_Type;
/* ── Pin config ──────────────────────────────────────────────────────────── */
typedef struct typedef struct
{ {
gpio_pin_direction_t direction; gpio_pin_direction_t direction;
@ -35,6 +66,21 @@ typedef struct
gpio_interrupt_mode_t interruptMode; gpio_interrupt_mode_t interruptMode;
} gpio_pin_config_t; } 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_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);
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);

View file

@ -0,0 +1,8 @@
#pragma once
/**
* Stub fsl_iomuxc.h для host-тестов.
* В реальном SDK содержит только inline-функции для настройки pin mux.
* В opto.c используется как транзитивный include прямых вызовов нет,
* поэтому stub пустой.
*/

View file

@ -3,20 +3,17 @@
/** /**
* Stub pin_mux.h для host-тестов. * Stub pin_mux.h для host-тестов.
* Макросы пинов зеркалят реальные значения из generated/pin_mux.h. * Макросы пинов зеркалят реальные значения из generated/pin_mux.h.
* GPIO-инстансы статические объекты-заглушки, адреса уникальны. * GPIO-инстансы приходят из fsl_gpio.h (stub_GPIO1/2/3).
*/ */
#include "fsl_gpio.h" #include "fsl_gpio.h"
/* Заглушки GPIO-периферии */
static GPIO_Type stub_GPIO3;
/* UserLed1 → GPIO3 pin 3 (LED_HEARTBEAT) */ /* 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_GPIO_PIN 3U
#define BOARD_INITPINS_UserLed1_INIT_GPIO_VALUE 1U #define BOARD_INITPINS_UserLed1_INIT_GPIO_VALUE 1U
/* UserLed2 → GPIO3 pin 4 (LED_APP) */ /* 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_GPIO_PIN 4U
#define BOARD_INITPINS_UserLed2_INIT_GPIO_VALUE 1U #define BOARD_INITPINS_UserLed2_INIT_GPIO_VALUE 1U

View file

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