diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f96f1d4..f5344e2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -12,25 +12,22 @@ "extensions": [ "actboy168.tasks", "bierner.markdown-preview-github-styles", - "brobeson.ctest-lab", "cheshirekow.cmake-format", - "codeium.codeium", "cschlosser.doxdocgen", "dan-c-underwood.arm", "davidanson.vscode-markdownlint", "gruntfuggly.todo-tree", - "kylinideteam.cmake-intellisence", + "ms-vscode.cmake-tools", "llvm-vs-code-extensions.vscode-clangd", "mcu-debug.debug-tracker-vscode", "mcu-debug.memory-view", "mcu-debug.peripheral-viewer", + "brobeson.ctest-lab", "mcu-debug.rtos-views", "ms-vscode.hexeditor", - "ms-vscode.makefile-tools", - "probe-rs.probe-rs-debugger", + "nefrob.vscode-just-syntax", "rioj7.command-variable", "trond-snekvik.gnu-mapfiles", - "twxs.cmake", "usernamehw.errorlens", "wayou.vscode-todo-highlight", "xaver.clang-format", diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e9f76b..2b0faaa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,7 +23,7 @@ endif() # ----------------------------------------------------------------------------- add_subdirectory(sdk) add_subdirectory(bsp) - +add_subdirectory(utils) # ----------------------------------------------------------------------------- # Сторонние и внутренние библиотеки # ----------------------------------------------------------------------------- diff --git a/CMakePresets.json b/CMakePresets.json index eddb87c..f50f8b4 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -128,7 +128,9 @@ "displayName": "Host Tests — Debug", "configurePreset": "host-debug", "targets": [ - "test_bsp_led" + "test_bsp_led", + "test_ring_buffer", + "test_timeout_pattern" ] }, { @@ -136,7 +138,9 @@ "displayName": "Host Tests — Release", "configurePreset": "host-release", "targets": [ - "test_bsp_led" + "test_bsp_led", + "test_ring_buffer", + "test_timeout_pattern" ] } ], diff --git a/bsp/CMakeLists.txt b/bsp/CMakeLists.txt index 01f7af5..56298a3 100644 --- a/bsp/CMakeLists.txt +++ b/bsp/CMakeLists.txt @@ -12,6 +12,7 @@ add_library( generated/startup/startup_MIMXRT1052.S) add_subdirectory(led) +add_subdirectory(tick) # Подавляем предупреждения при компиляции собственных .c файлов библиотеки target_compile_options(bsp_board PRIVATE -w) diff --git a/bsp/generated/board.h b/bsp/generated/board.h index 275c12b..2ff1e4a 100644 --- a/bsp/generated/board.h +++ b/bsp/generated/board.h @@ -4,7 +4,7 @@ /*! @brief The board name */ #define BOARD_NAME "TFT4.3_UB_BOARD" -/*! //TODO: @brief The board name */ +/*! @brief The board name */ void board_hw_init(void); #endif /* _BOARD_H_ */ \ No newline at end of file diff --git a/bsp/led/README.md b/bsp/led/README.md index 07aecc4..b2fc7db 100644 --- a/bsp/led/README.md +++ b/bsp/led/README.md @@ -19,13 +19,13 @@ ## API ```c -void led_init(void); // вызвать один раз после BOARD_InitPins() +void bsp_led_init(void); // вызвать один раз после board_hw_init() -void led_on(led_id_t id); -void led_off(led_id_t id); -void led_toggle(led_id_t id); -void led_set(led_id_t id, bool on); -bool led_get(led_id_t id); +void bsp_led_on(led_id_t id); +void bsp_led_off(led_id_t id); +void bsp_led_toggle(led_id_t id); +void bsp_led_set(led_id_t id, bool on); +bool bsp_led_get(led_id_t id); ``` --- @@ -36,14 +36,14 @@ bool led_get(led_id_t id); #include "bsp/led.h" // инициализация -led_init(); +bsp_led_init(); // heartbeat -led_toggle(LED_HEARTBEAT); +bsp_led_toggle(LED_HEARTBEAT); // прикладная индикация -led_on(LED_APP); // пакет принят / тест запущен -led_off(LED_APP); // сброс +bsp_led_on(LED_APP); // пакет принят / тест запущен +bsp_led_off(LED_APP); // сброс ``` --- diff --git a/bsp/led/include/bsp/led.h b/bsp/led/include/bsp/led.h index a82309b..73a3a39 100644 --- a/bsp/led/include/bsp/led.h +++ b/bsp/led/include/bsp/led.h @@ -1,4 +1,5 @@ -#pragma once +#ifndef BSP_LED_H_ +#define BSP_LED_H_ /** * @file led.h @@ -7,7 +8,7 @@ * LED_HEARTBEAT — системный, мигает как признак жизни прошивки * LED_APP — прикладной, управляется из firmware по ситуации * - * Пины сконфигурированы в generated/pin_mux. Этот хедер не знает + * Пины сконфигурированы в generated/pin_mux.h. Этот хедер не знает * ни про GPIO-порты, ни про NXP SDK. */ @@ -25,25 +26,27 @@ typedef enum /** * @brief Инициализация обоих светодиодов. - * Вызвать один раз после BOARD_InitPins(). + * Вызвать один раз после board_init_hw(). * После вызова оба LED выключены. */ -void led_init(void); +void bsp_led_init(void); /** @brief Включить светодиод. */ -void led_on(led_id_t led_id); +void bsp_led_on(led_id_t led_id); /** @brief Выключить светодиод. */ -void led_off(led_id_t led_id); +void bsp_led_off(led_id_t led_id); /** @brief Переключить состояние светодиода. */ -void led_toggle(led_id_t led_id); +void bsp_led_toggle(led_id_t led_id); /** * @brief Установить состояние светодиода явно. * @param on true — включить, false — выключить */ -void led_set(led_id_t led_id, bool is_enabled); +void bsp_led_set(led_id_t led_id, bool is_enabled); /** @brief Получить текущее состояние (true — горит). */ -bool led_get(led_id_t led_id); \ No newline at end of file +bool bsp_led_get(led_id_t led_id); + +#endif //BSP_LED_H_ \ No newline at end of file diff --git a/bsp/led/src/led.c b/bsp/led/src/led.c index 016b1ec..9494f4e 100644 --- a/bsp/led/src/led.c +++ b/bsp/led/src/led.c @@ -47,19 +47,19 @@ static led_desc_t s_leds[] = { /* ── Приватные хелперы ───────────────────────────────────────────────── */ /** Перевести логическое состояние в физический уровень GPIO (active LOW). */ -static inline uint8_t led_to_gpio_level(bool is_enabled) +static inline uint8_t bsp_led_to_gpio_level(bool is_enabled) { return is_enabled ? 0U : 1U; } -static inline void led_apply(led_desc_t *p_led) +static inline void bsp_led_apply(led_desc_t *p_led) { - GPIO_PinWrite(p_led->gpio, p_led->pin, led_to_gpio_level(p_led->state)); + GPIO_PinWrite(p_led->gpio, p_led->pin, bsp_led_to_gpio_level(p_led->state)); } /* ── Публичный API ───────────────────────────────────────────────────── */ -void led_init(void) +void bsp_led_init(void) { gpio_pin_config_t cfg = { .direction = kGPIO_DigitalOutput, @@ -74,28 +74,28 @@ void led_init(void) } } -void led_on(led_id_t led_id) +void bsp_led_on(led_id_t led_id) { - led_set(led_id, true); + bsp_led_set(led_id, true); } -void led_off(led_id_t led_id) +void bsp_led_off(led_id_t led_id) { - led_set(led_id, false); + bsp_led_set(led_id, false); } -void led_toggle(led_id_t led_id) +void bsp_led_toggle(led_id_t led_id) { - led_set(led_id, !s_leds[led_id].state); + bsp_led_set(led_id, !s_leds[led_id].state); } -void led_set(led_id_t led_id, bool is_enabled) +void bsp_led_set(led_id_t led_id, bool is_enabled) { s_leds[led_id].state = is_enabled; - led_apply(&s_leds[led_id]); + bsp_led_apply(&s_leds[led_id]); } -bool led_get(led_id_t led_id) +bool bsp_led_get(led_id_t led_id) { return s_leds[led_id].state; } \ No newline at end of file diff --git a/bsp/tick/CMakeLists.txt b/bsp/tick/CMakeLists.txt new file mode 100644 index 0000000..95f8221 --- /dev/null +++ b/bsp/tick/CMakeLists.txt @@ -0,0 +1,14 @@ +if(BUILD_TESTS_HOST) + return() +endif() + +add_library(bsp_tick STATIC src/tick.c) + +target_include_directories( + bsp_tick + PUBLIC include/ + PRIVATE src/) + +target_link_libraries( + bsp_tick PUBLIC bsp_board # board.h, pin_mux.h — транзитивно +) diff --git a/bsp/tick/README.md b/bsp/tick/README.md new file mode 100644 index 0000000..d5676ed --- /dev/null +++ b/bsp/tick/README.md @@ -0,0 +1,138 @@ +# bsp_tick — интеграция в firmware-проекты + +`SysTick_Handler` определён внутри `tick.c` и принадлежит модулю `bsp_tick`. +Для добавления внешней логики в обработчик используй слабый хук (см. ниже). + +--- + +## firmware/test и firmware/bootloader (bare-metal) + +**CMakeLists.txt**: + +```cmake +target_link_libraries(firmware_test PRIVATE bsp_tick) +``` + +**Порядок инициализации в main()**: + +```c +board_hw_init(); // тактирование и пины (BOARD_BootClockRUN внутри) +bsp_tick_init(); // SysTick — после того как SystemCoreClock актуален +bsp_uart_init(115200); // и далее всё что зависит от времени +``` + +--- + +## firmware/tft_app (FreeRTOS) + +**FreeRTOSConfig.h** — убедиться: + +```c +#define configTICK_RATE_HZ 1000 // 1 тик = 1 мс +#define configUSE_TICK_HOOK 1 // включить vApplicationTickHook +``` + +**board.c** — добавить hook (см. раздел про хуки ниже): + +```c +#include "bsp/tick.h" + +void vApplicationTickHook(void) +{ + bsp_tick_inc(); // no-op в FreeRTOS-режиме, но оставляем для единообразия +} +``` + +**CMakeLists.txt**: + +```cmake +target_link_libraries(tft_app PRIVATE bsp_tick freertos_kernel) +target_compile_definitions(tft_app PRIVATE BSP_TICK_FREERTOS_MODE) +``` + +**Инициализация**: + +```c +board_hw_init(); +bsp_tick_init(); // no-op, но вызываем для симметрии с bare-metal +vTaskStartScheduler(); // FreeRTOS берёт SysTick себе здесь +``` + +--- + +## Добавление логики в SysTick — weak hook + +Каждый BSP-модуль владеет своим прерыванием целиком. `SysTick_Handler` +живёт в `tick.c` и принадлежит `bsp_tick`. Если другому модулю нужно +выполнять работу каждый тик (watchdog, программный таймер и т.п.) — +используй слабый хук, не трогая `tick.c`. + +**Как это работает:** + +`tick.c` объявляет и вызывает `bsp_systick_hook()` с атрибутом `weak`. +Если никто не определил эту функцию — линкер подставляет пустую +заглушку, накладные расходы нулевые. Как только в любом `.c` файле +проекта появляется сильное определение — оно автоматически подхватывается. + +```c +// tick.c (уже реализовано): +__attribute__((weak)) void bsp_systick_hook(void) { /* no-op по умолчанию */ } + +void SysTick_Handler(void) +{ + bsp_tick_inc(); + bsp_systick_hook(); +} +``` + +### **Пример: watchdog из board.c** + +```c +// board.c +#include "bsp/tick.h" +#include "bsp/wdog.h" + +// Переопределяем слабый хук — линкер возьмёт эту версию +void bsp_systick_hook(void) +{ + bsp_wdog_feed(); +} +``` + +### **Пример: два действия в хуке** + +```c +// board.c +void bsp_systick_hook(void) +{ + bsp_wdog_feed(); + bsp_some_other_periodic_task(); +} +``` + +**Важно:** хук вызывается из ISR-контекста. Никаких блокирующих +операций, мьютексов или `bsp_delay()` внутри. + +--- + +## Использование в BSP-модулях и приложении + +```c +#include "bsp/tick.h" + +// Таймаут (wraparound-safe): +uint32_t start = bsp_tick_get_ms(); +while (!done) { + if ((bsp_tick_get_ms() - start) >= TIMEOUT_MS) { break; } +} + +// Блокирующая пауза (инициализация, datasheet-задержки): +bsp_delay(10); + +// Периодическое действие без блокировки основного цикла: +static uint32_t s_last_ms = 0U; +if ((bsp_tick_get_ms() - s_last_ms) >= 500U) { + s_last_ms = bsp_tick_get_ms(); + // ... действие ... +} +``` diff --git a/bsp/tick/include/bsp/tick.h b/bsp/tick/include/bsp/tick.h new file mode 100644 index 0000000..66dd8ba --- /dev/null +++ b/bsp/tick/include/bsp/tick.h @@ -0,0 +1,103 @@ +/** + * @file bsp/tick.h + * @brief Монотонный миллисекундный счётчик для BSP. + * + * Предоставляет единый API времени для всех BSP-модулей + * (bsp_uart, bsp_delay, watchdog и др.) в двух режимах сборки: + * + * Bare-metal (по умолчанию): + * - bsp_tick_init() конфигурирует SysTick на 1 мс. + * - SysTick_Handler() в board.c должен вызывать bsp_tick_inc(). + * - bsp_tick_get_ms() читает внутренний volatile-счётчик. + * + * FreeRTOS (#define BSP_TICK_FREERTOS_MODE): + * - bsp_tick_init() — no-op (SysTick принадлежит FreeRTOS). + * - vApplicationTickHook() в board.c должен вызывать bsp_tick_inc(). + * - bsp_tick_get_ms() возвращает xTaskGetTickCount(). + * - Предполагает configTICK_RATE_HZ = 1000 (1 тик = 1 мс). + * + * Использование в firmware/CMakeLists.txt: + * target_compile_definitions(firmware_test PRIVATE) // bare-metal + * target_compile_definitions(tft_app PRIVATE BSP_TICK_FREERTOS_MODE) + */ + +#ifndef BSP_TICK_H +#define BSP_TICK_H + +#include + +/* ── Инициализация ───────────────────────────────────────────────────── */ + +/** + * @brief Инициализировать источник тиков. + * + * Bare-metal: настраивает SysTick на прерывание каждые 1 мс. + * FreeRTOS: no-op — SysTick уже настроен планировщиком. + * + * Вызывать после board_hw_init(), до любого использования + * bsp_tick_get_ms() или bsp_delay(). + */ +void bsp_tick_init(void); + +/* ── Инкремент (из обработчика прерывания) ───────────────────────────── */ + +/** + * @brief Увеличить счётчик на 1 мс. + * + * Bare-metal: вызывать из SysTick_Handler(). + * FreeRTOS: вызывать из vApplicationTickHook(). + * + * @note В FreeRTOS-режиме функция — no-op (счётчик не используется), + * но вызов из hook-а оставляем для единообразия кода board.c. + */ +void bsp_tick_inc(void); + +/* ── Чтение времени ──────────────────────────────────────────────────── */ + +/** + * @brief Вернуть текущее время в миллисекундах. + * + * Bare-metal: монотонный счётчик, инкрементируемый из SysTick_Handler. + * Переполняется через ~49.7 дней (UINT32_MAX мс). + * FreeRTOS: возвращает xTaskGetTickCount() (при configTICK_RATE_HZ=1000 + * значение в мс совпадает с тиками). + * + * Для измерения интервалов используй разность: + * uint32_t start = bsp_tick_get_ms(); + * // ... работа ... + * uint32_t elapsed = bsp_tick_get_ms() - start; // корректно при wraparound + */ +uint32_t bsp_tick_get_ms(void); + +/* ── Задержка ────────────────────────────────────────────────────────── */ + +/** + * @brief Блокирующая задержка. + * + * Реализована через polling bsp_tick_get_ms() — не использует + * RTOS-примитивов, работает в любом контексте включая ISR + * (при условии что SysTick прерывания не заблокированы). + * + * @warning В FreeRTOS-контексте блокирует задачу без освобождения + * процессора. Для FreeRTOS предпочитай vTaskDelay(). + * bsp_delay() здесь — для BSP-инициализации до старта + * планировщика и для bare-metal firmware. + * + * @param millis Время задержки в миллисекундах. + */ +void bsp_delay(uint32_t millis); + +#endif /* BSP_TICK_H */ + +/* ── Хук для внешней логики ──────────────────────────────────────────── */ + +/** + * @brief Вызывается из SysTick_Handler (bare-metal) или + * vApplicationTickHook (FreeRTOS) после bsp_tick_inc(). + * + * Слабое определение по умолчанию — no-op. + * Переопредели в своём .c файле для watchdog, программных таймеров и т.п. + * + * @warning ISR-контекст. Никаких блокировок и задержек. + */ +void bsp_systick_hook(void); \ No newline at end of file diff --git a/bsp/tick/src/tick.c b/bsp/tick/src/tick.c new file mode 100644 index 0000000..9490d89 --- /dev/null +++ b/bsp/tick/src/tick.c @@ -0,0 +1,101 @@ +/** + * @file tick.c + * @brief Реализация BSP tick — SysTick (bare-metal) или FreeRTOS. + * + * SysTick_Handler определён здесь и принадлежит этому модулю. + * Для добавления внешней логики в обработчик используй bsp_systick_hook(). + */ + +#include "bsp/tick.h" + +/* ── Слабый хук для внешней логики в SysTick ─────────────────────────── */ + +/** + * @brief Хук, вызываемый из SysTick_Handler после bsp_tick_inc(). + * + * По умолчанию — no-op. Переопредяется в своём .c файле (без weak) + * чтобы добавить периодическую работу: watchdog, программный таймер и т.п. + * + * @warning Вызывается из ISR-контекста. Никаких блокировок и задержек. + */ +__attribute__((weak)) void bsp_systick_hook(void) +{ /* no-op */ +} + +/* ── Bare-metal реализация ───────────────────────────────────────────── */ + +#ifndef BSP_TICK_FREERTOS_MODE + +#include "fsl_common.h" /* SystemCoreClock, SysTick_Config */ + +static volatile uint32_t s_tick_ms = 0U; + +void bsp_tick_init(void) +{ + SysTick_Config(SystemCoreClock / 1000U); +} + +void bsp_tick_inc(void) +{ + s_tick_ms++; +} + +uint32_t bsp_tick_get_ms(void) +{ + return s_tick_ms; +} + +void SysTick_Handler(void) +{ + bsp_tick_inc(); + bsp_systick_hook(); +} + +/* ── FreeRTOS реализация ─────────────────────────────────────────────── */ + +#else /* BSP_TICK_FREERTOS_MODE */ + +#include "FreeRTOS.h" +#include "task.h" + +#if configTICK_RATE_HZ != 1000 +#warning "BSP_TICK_FREERTOS_MODE: configTICK_RATE_HZ != 1000. \ +bsp_tick_get_ms() returns ticks, not milliseconds." +#endif + +void bsp_tick_init(void) +{ /* no-op */ +} +void bsp_tick_inc(void) +{ /* no-op */ +} + +uint32_t bsp_tick_get_ms(void) +{ + return (uint32_t) xTaskGetTickCount(); +} + +/* + * В FreeRTOS-режиме SysTick_Handler принадлежит планировщику. + * Периодическая логика идёт через vApplicationTickHook в board.c, + * который вызывает bsp_tick_inc() (no-op) и bsp_systick_hook(). + * + * board.c: + * void vApplicationTickHook(void) { + * bsp_tick_inc(); + * bsp_systick_hook(); + * } + */ + +#endif /* BSP_TICK_FREERTOS_MODE */ + +/* ── bsp_delay — общий для обоих режимов ────────────────────────────── */ + +void bsp_delay(uint32_t millis) +{ + uint32_t start = bsp_tick_get_ms(); + while ((bsp_tick_get_ms() - start) < millis) + { + /* busy-wait */ + } +} \ No newline at end of file diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 211b2f2..cba7799 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -12,7 +12,7 @@ target_sources(firmware_test # ----------------------------------------------------------------------------- target_link_libraries( firmware_test - PRIVATE bsp_board bsp_led + PRIVATE bsp_board bsp_led bsp_tick # Опциональные библиотеки lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED ) diff --git a/firmware/test/main.c b/firmware/test/main.c index 032d4da..265b9a0 100644 --- a/firmware/test/main.c +++ b/firmware/test/main.c @@ -1,14 +1,19 @@ #include "board.h" #include "bsp/led.h" +#include "bsp/tick.h" int main(void) { - board_hw_init(); - led_init(); - led_on(LED_HEARTBEAT); - led_off(LED_APP); + board_hw_init(); + bsp_led_init(); + bsp_tick_init(); + const uint16_t DELAY_MS = 1000; while (1) { + bsp_led_on(LED_HEARTBEAT); + bsp_delay(DELAY_MS); + bsp_led_off(LED_HEARTBEAT); + bsp_delay(DELAY_MS); } } diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index b33f895..eada118 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -3,28 +3,34 @@ # ----------------------------------------------------------------------------- # Вспомогательная функция добавления host-теста. # -# Использование: add_host_test( NAME <имя теста> SOURCES <тест.c> -# <тестируемый_модуль.c> ... MOCKS <путь к папке с stub-хедерами> # -# опционально ) +# Использование: add_host_test( NAME <имя теста> SOURCES <тест.c> [<модуль.c> +# ...] # исходники теста + тестируемый код INCLUDES [ ...] # +# дополнительные include-пути (опц.) MOCKS [ ...] # папки +# со stub-хедерами (опц.) ) # # Каждый тест — отдельный исполняемый файл. CTest запускает их все. +# +# Соглашения: - lib_external подключается всегда (Unity + fff). - INCLUDES и +# MOCKS — явные: каждый тест сам декларирует свои зависимости. Нет скрытых +# глобальных include-путей, специфичных для одного модуля. # ----------------------------------------------------------------------------- function(add_host_test) - cmake_parse_arguments(ARG "" "NAME" "SOURCES;MOCKS" ${ARGN}) + cmake_parse_arguments(ARG "" "NAME" "SOURCES;INCLUDES;MOCKS" ${ARGN}) add_executable(${ARG_NAME} ${ARG_SOURCES}) target_link_libraries(${ARG_NAME} PRIVATE lib_external) - # stub-хедеры подключаются как PRIVATE — не торчат в других таргетах + # Явные include-пути, специфичные для теста + foreach(inc_dir IN LISTS ARG_INCLUDES) + target_include_directories(${ARG_NAME} PRIVATE ${inc_dir}) + endforeach() + + # Stub-хедеры для моков (PRIVATE — не торчат в других таргетах) foreach(mock_dir IN LISTS ARG_MOCKS) target_include_directories(${ARG_NAME} PRIVATE ${mock_dir}) endforeach() - # публичные хедеры BSP нужны для #include "bsp/led.h" - target_include_directories(${ARG_NAME} - PRIVATE ${PROJECT_SOURCE_DIR}/bsp/led/include) - add_test( NAME ${ARG_NAME} COMMAND ${ARG_NAME} @@ -44,8 +50,31 @@ add_host_test( NAME test_bsp_led SOURCES - test_bsp_led.c - ${PROJECT_SOURCE_DIR}/bsp/led/src/led.c # тестируемый модуль напрямую + led/test_bsp_led.c + ${PROJECT_SOURCE_DIR}/bsp/led/src/led.c + INCLUDES + ${PROJECT_SOURCE_DIR}/bsp/led/include # для #include "bsp/led.h" MOCKS ${BSP_MOCKS_DIR} # fsl_gpio.h, pin_mux.h, board.h ) + +# ----------------------------------------------------------------------------- +# utils/ring_buffer +# ----------------------------------------------------------------------------- +add_host_test( + NAME + test_ring_buffer + SOURCES + ring_buffer/test_ring_buffer.c + ${PROJECT_SOURCE_DIR}/utils/ring_buffer/ring_buffer.c + INCLUDES + ${PROJECT_SOURCE_DIR}/utils # для #include "ring_buffer/ring_buffer.h" +) + +# ----------------------------------------------------------------------------- +# Тест таймаут-паттерна, используемого в bsp/tick +# ----------------------------------------------------------------------------- +add_host_test( + NAME test_timeout_pattern SOURCES timeout/test_timeout_pattern.c + # нет INCLUDES и MOCKS — чистая математика, нет зависимостей +) diff --git a/tests/host/README.md b/tests/host/README.md index ec357fe..16b7858 100644 --- a/tests/host/README.md +++ b/tests/host/README.md @@ -80,9 +80,9 @@ tests/host/ │ ├── fsl_lpuart.h │ ├── pin_mux.h # зеркало макросов пинов из generated/ │ └── board.h -├── test_led.c # категория B — BSP-модуль -├── test_protocol.c # категория A — платформонезависимый -└── test_runner_logic.c # категория A +├── led/test_led.c # категория B — BSP-модуль +├── protocol/test_protocol.c # категория A — платформонезависимый +└── runner/test_runner_logic.c # категория A ``` `mocks/` подключается как include path с более высоким приоритетом чем `sdk/`. Компилятор найдёт `fsl_gpio.h` из `mocks/` раньше чем из SDK — `led.c` компилируется на хосте без единого изменения в BSP-коде. diff --git a/tests/host/test_bsp_led.c b/tests/host/led/test_bsp_led.c similarity index 55% rename from tests/host/test_bsp_led.c rename to tests/host/led/test_bsp_led.c index c9dfaaf..c588367 100644 --- a/tests/host/test_bsp_led.c +++ b/tests/host/led/test_bsp_led.c @@ -13,11 +13,11 @@ FAKE_VOID_FUNC(GPIO_PinWrite, GPIO_Type *, uint32_t, uint8_t); /* ── Capture для gpio_pin_config_t ────────────────────────────────── * fff хранит arg2_history как указатель — но cfg живёт на стеке - * led_init() и становится dangling после возврата. + * bsp_led_init() и становится dangling после возврата. * (ASAN правильно детектирует это как stack-use-after-return) * * Решение: custom_fake копирует структуру по значению в момент вызова, - * пока стек led_init() ещё жив. + * пока стек bsp_led_init() ещё жив. * ─────────────────────────────────────────────────────────────────── */ static gpio_pin_config_t s_captured_cfg[2]; static int s_capture_idx = 0; @@ -43,11 +43,11 @@ void setUp(void) RESET_FAKE(GPIO_PinWrite); FFF_RESET_HISTORY(); - /* подключаем capture до вызова led_init */ + /* подключаем capture до вызова bsp_led_init */ s_capture_idx = 0; GPIO_PinInit_fake.custom_fake = GPIO_PinInit_capture; - led_init(); + bsp_led_init(); } void tearDown(void) @@ -56,110 +56,110 @@ void tearDown(void) /* ── Тесты инициализации ─────────────────────────────────────────────── */ -void test_led_init_calls_gpio_init_for_each_led(void) +void test_bsp_led_init_calls_gpio_init_for_each_led(void) { TEST_ASSERT_EQUAL(2, GPIO_PinInit_fake.call_count); } -void test_led_init_configures_as_output(void) +void test_bsp_led_init_configures_as_output(void) { - /* s_captured_cfg — копия по значению, стек led_init() не нужен */ + /* s_captured_cfg — копия по значению, стек bsp_led_init() не нужен */ TEST_ASSERT_EQUAL(kGPIO_DigitalOutput, s_captured_cfg[0].direction); TEST_ASSERT_EQUAL(kGPIO_DigitalOutput, s_captured_cfg[1].direction); } -void test_led_init_output_logic_is_high(void) +void test_bsp_led_init_output_logic_is_high(void) { /* active LOW — начальное состояние GPIO = 1 (LED выключен) */ TEST_ASSERT_EQUAL_UINT8(1U, s_captured_cfg[0].outputLogic); TEST_ASSERT_EQUAL_UINT8(1U, s_captured_cfg[1].outputLogic); } -void test_led_init_both_leds_off(void) +void test_bsp_led_init_both_leds_off(void) { - TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); - TEST_ASSERT_FALSE(led_get(LED_APP)); + TEST_ASSERT_FALSE(bsp_led_get(LED_HEARTBEAT)); + TEST_ASSERT_FALSE(bsp_led_get(LED_APP)); } -/* ── Тесты led_on / led_off ──────────────────────────────────────────── */ +/* ── Тесты bsp_led_on / bsp_led_off ──────────────────────────────────────────── */ -void test_led_on_sets_state_true(void) +void test_bsp_led_on_sets_state_true(void) { - led_on(LED_HEARTBEAT); - TEST_ASSERT_TRUE(led_get(LED_HEARTBEAT)); + bsp_led_on(LED_HEARTBEAT); + TEST_ASSERT_TRUE(bsp_led_get(LED_HEARTBEAT)); } -void test_led_off_sets_state_false(void) +void test_bsp_led_off_sets_state_false(void) { - led_on(LED_HEARTBEAT); - led_off(LED_HEARTBEAT); - TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); + bsp_led_on(LED_HEARTBEAT); + bsp_led_off(LED_HEARTBEAT); + TEST_ASSERT_FALSE(bsp_led_get(LED_HEARTBEAT)); } -void test_led_on_writes_gpio_low(void) +void test_bsp_led_on_writes_gpio_low(void) { /* active LOW: включить LED = записать 0 в GPIO */ - led_on(LED_HEARTBEAT); + bsp_led_on(LED_HEARTBEAT); TEST_ASSERT_EQUAL_UINT8(0U, GPIO_PinWrite_fake.arg2_val); } -void test_led_off_writes_gpio_high(void) +void test_bsp_led_off_writes_gpio_high(void) { /* active LOW: выключить LED = записать 1 в GPIO */ - led_off(LED_HEARTBEAT); + bsp_led_off(LED_HEARTBEAT); TEST_ASSERT_EQUAL_UINT8(1U, GPIO_PinWrite_fake.arg2_val); } -/* ── Тесты led_toggle ────────────────────────────────────────────────── */ +/* ── Тесты bsp_led_toggle ────────────────────────────────────────────────── */ -void test_led_toggle_off_to_on(void) +void test_bsp_led_toggle_off_to_on(void) { - TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); - led_toggle(LED_HEARTBEAT); - TEST_ASSERT_TRUE(led_get(LED_HEARTBEAT)); + TEST_ASSERT_FALSE(bsp_led_get(LED_HEARTBEAT)); + bsp_led_toggle(LED_HEARTBEAT); + TEST_ASSERT_TRUE(bsp_led_get(LED_HEARTBEAT)); } -void test_led_toggle_on_to_off(void) +void test_bsp_led_toggle_on_to_off(void) { - led_on(LED_HEARTBEAT); - led_toggle(LED_HEARTBEAT); - TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); + bsp_led_on(LED_HEARTBEAT); + bsp_led_toggle(LED_HEARTBEAT); + TEST_ASSERT_FALSE(bsp_led_get(LED_HEARTBEAT)); } -void test_led_toggle_twice_returns_to_initial(void) +void test_bsp_led_toggle_twice_returns_to_initial(void) { - led_toggle(LED_HEARTBEAT); - led_toggle(LED_HEARTBEAT); - TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); + bsp_led_toggle(LED_HEARTBEAT); + bsp_led_toggle(LED_HEARTBEAT); + TEST_ASSERT_FALSE(bsp_led_get(LED_HEARTBEAT)); } -/* ── Тесты led_set ───────────────────────────────────────────────────── */ +/* ── Тесты bsp_led_set ───────────────────────────────────────────────────── */ -void test_led_set_true_turns_on(void) +void test_bsp_led_set_true_turns_on(void) { - led_set(LED_APP, true); - TEST_ASSERT_TRUE(led_get(LED_APP)); + bsp_led_set(LED_APP, true); + TEST_ASSERT_TRUE(bsp_led_get(LED_APP)); } -void test_led_set_false_turns_off(void) +void test_bsp_led_set_false_turns_off(void) { - led_set(LED_APP, true); - led_set(LED_APP, false); - TEST_ASSERT_FALSE(led_get(LED_APP)); + bsp_led_set(LED_APP, true); + bsp_led_set(LED_APP, false); + TEST_ASSERT_FALSE(bsp_led_get(LED_APP)); } /* ── Тест независимости LED друг от друга ───────────────────────────── */ void test_leds_are_independent(void) { - led_on(LED_HEARTBEAT); - TEST_ASSERT_TRUE(led_get(LED_HEARTBEAT)); - TEST_ASSERT_FALSE(led_get(LED_APP)); + bsp_led_on(LED_HEARTBEAT); + TEST_ASSERT_TRUE(bsp_led_get(LED_HEARTBEAT)); + TEST_ASSERT_FALSE(bsp_led_get(LED_APP)); - led_on(LED_APP); - led_off(LED_HEARTBEAT); - TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); - TEST_ASSERT_TRUE(led_get(LED_APP)); + bsp_led_on(LED_APP); + bsp_led_off(LED_HEARTBEAT); + TEST_ASSERT_FALSE(bsp_led_get(LED_HEARTBEAT)); + TEST_ASSERT_TRUE(bsp_led_get(LED_APP)); } /* ── main ────────────────────────────────────────────────────────────── */ @@ -168,22 +168,22 @@ int main(void) { UNITY_BEGIN(); - RUN_TEST(test_led_init_calls_gpio_init_for_each_led); - RUN_TEST(test_led_init_configures_as_output); - RUN_TEST(test_led_init_output_logic_is_high); - RUN_TEST(test_led_init_both_leds_off); + RUN_TEST(test_bsp_led_init_calls_gpio_init_for_each_led); + RUN_TEST(test_bsp_led_init_configures_as_output); + RUN_TEST(test_bsp_led_init_output_logic_is_high); + RUN_TEST(test_bsp_led_init_both_leds_off); - RUN_TEST(test_led_on_sets_state_true); - RUN_TEST(test_led_off_sets_state_false); - RUN_TEST(test_led_on_writes_gpio_low); - RUN_TEST(test_led_off_writes_gpio_high); + RUN_TEST(test_bsp_led_on_sets_state_true); + RUN_TEST(test_bsp_led_off_sets_state_false); + RUN_TEST(test_bsp_led_on_writes_gpio_low); + RUN_TEST(test_bsp_led_off_writes_gpio_high); - RUN_TEST(test_led_toggle_off_to_on); - RUN_TEST(test_led_toggle_on_to_off); - RUN_TEST(test_led_toggle_twice_returns_to_initial); + RUN_TEST(test_bsp_led_toggle_off_to_on); + RUN_TEST(test_bsp_led_toggle_on_to_off); + RUN_TEST(test_bsp_led_toggle_twice_returns_to_initial); - RUN_TEST(test_led_set_true_turns_on); - RUN_TEST(test_led_set_false_turns_off); + RUN_TEST(test_bsp_led_set_true_turns_on); + RUN_TEST(test_bsp_led_set_false_turns_off); RUN_TEST(test_leds_are_independent); diff --git a/tests/host/ring_buffer/test_ring_buffer.c b/tests/host/ring_buffer/test_ring_buffer.c new file mode 100644 index 0000000..ddab7b9 --- /dev/null +++ b/tests/host/ring_buffer/test_ring_buffer.c @@ -0,0 +1,459 @@ +/** + * @file test_ring_buffer.c + * @brief Unit-тесты для ring_buffer (Unity framework). + * + * Группы тестов: + * 1. Init — корректная и некорректная инициализация + * 2. Put/Get — базовые операции с одним байтом + * 3. Write/Read — блочные операции + * 4. Full/Empty — граничные состояния + * 5. Wraparound — корректность при переполнении индексов + * 6. SPSC sim — имитация одновременной записи ISR и чтения consumer + * 7. Reset — сброс состояния + */ + +#include "ring_buffer/ring_buffer.h" +#include "unity.h" + +#include + +/* ── Вспомогательные объекты ─────────────────────────────────────────── */ + +#define BUF_SIZE 8U /* Степень двойки, удобна для wraparound-тестов. */ + +static ring_buffer_desc_t rb; +static uint8_t storage[BUF_SIZE]; + +void setUp(void) +{ + /* Вызывается перед каждым тестом — чистое состояние. */ + bool ok = ring_buffer_init(&rb, storage, BUF_SIZE); + TEST_ASSERT_TRUE(ok); +} + +void tearDown(void) +{ /* ничего */ +} + +/* ═══════════════════════════════════════════════════════════════════════ + * 1. Init + * ═══════════════════════════════════════════════════════════════════════ */ + +void test_init_valid(void) +{ + ring_buffer_desc_t tmp; + uint8_t mem[16]; + TEST_ASSERT_TRUE(ring_buffer_init(&tmp, mem, 16U)); + TEST_ASSERT_TRUE(ring_buffer_is_empty(&tmp)); + TEST_ASSERT_EQUAL_size_t(0U, ring_buffer_count(&tmp)); + TEST_ASSERT_EQUAL_size_t(16U, ring_buffer_free(&tmp)); +} + +void test_init_rejects_null_rb(void) +{ + uint8_t mem[8]; + TEST_ASSERT_FALSE(ring_buffer_init(NULL, mem, 8U)); +} + +void test_init_rejects_null_buf(void) +{ + ring_buffer_desc_t tmp; + TEST_ASSERT_FALSE(ring_buffer_init(&tmp, NULL, 8U)); +} + +void test_init_rejects_zero_size(void) +{ + ring_buffer_desc_t tmp; + uint8_t mem[8]; + TEST_ASSERT_FALSE(ring_buffer_init(&tmp, mem, 0U)); +} + +void test_init_rejects_non_power_of_two(void) +{ + ring_buffer_desc_t tmp; + uint8_t mem[10]; + + TEST_ASSERT_FALSE(ring_buffer_init(&tmp, mem, 3U)); + TEST_ASSERT_FALSE(ring_buffer_init(&tmp, mem, 5U)); + TEST_ASSERT_FALSE(ring_buffer_init(&tmp, mem, 6U)); + TEST_ASSERT_FALSE(ring_buffer_init(&tmp, mem, 7U)); +} + +void test_init_accepts_all_valid_powers_of_two(void) +{ + ring_buffer_desc_t tmp; + uint8_t mem[256]; + + for (size_t s = 1U; s <= 256U; s <<= 1U) + { + TEST_ASSERT_TRUE(ring_buffer_init(&tmp, mem, s)); + } +} + +/* ═══════════════════════════════════════════════════════════════════════ + * 2. Put / Get — одиночные байты + * ═══════════════════════════════════════════════════════════════════════ */ + +void test_put_get_single_byte(void) +{ + uint8_t out = 0xFF; + + TEST_ASSERT_TRUE(ring_buffer_put(&rb, 0xAB)); + TEST_ASSERT_EQUAL_size_t(1U, ring_buffer_count(&rb)); + TEST_ASSERT_FALSE(ring_buffer_is_empty(&rb)); + + TEST_ASSERT_TRUE(ring_buffer_get(&rb, &out)); + TEST_ASSERT_EQUAL_UINT8(0xAB, out); + TEST_ASSERT_TRUE(ring_buffer_is_empty(&rb)); +} + +void test_get_from_empty_returns_false(void) +{ + uint8_t out = 0xAA; + TEST_ASSERT_FALSE(ring_buffer_get(&rb, &out)); + /* out не должен быть изменён */ + TEST_ASSERT_EQUAL_UINT8(0xAA, out); +} + +void test_put_returns_false_when_full(void) +{ + /* Заполнить буфер до краёв */ + for (uint8_t i = 0; i < BUF_SIZE; i++) + { + TEST_ASSERT_TRUE(ring_buffer_put(&rb, i)); + } + TEST_ASSERT_TRUE(ring_buffer_is_full(&rb)); + + /* Следующий put должен провалиться */ + TEST_ASSERT_FALSE(ring_buffer_put(&rb, 0xFF)); + /* Данные при этом не повреждены */ + TEST_ASSERT_EQUAL_size_t(BUF_SIZE, ring_buffer_count(&rb)); +} + +void test_fifo_ordering(void) +{ + /* Проверяем порядок FIFO: первым вошёл — первым вышел. */ + for (uint8_t i = 1U; i <= 4U; i++) + { + ring_buffer_put(&rb, i); + } + + uint8_t out; + for (uint8_t i = 1U; i <= 4U; i++) + { + TEST_ASSERT_TRUE(ring_buffer_get(&rb, &out)); + TEST_ASSERT_EQUAL_UINT8(i, out); + } +} + +/* ═══════════════════════════════════════════════════════════════════════ + * 3. Write / Read — блочные операции + * ═══════════════════════════════════════════════════════════════════════ */ + +void test_write_read_full_block(void) +{ + const uint8_t src[] = { 0x01, 0x02, 0x03, 0x04 }; + uint8_t dst[4] = { 0 }; + + TEST_ASSERT_EQUAL_size_t(4U, ring_buffer_write(&rb, src, 4U)); + TEST_ASSERT_EQUAL_size_t(4U, ring_buffer_count(&rb)); + + TEST_ASSERT_EQUAL_size_t(4U, ring_buffer_read(&rb, dst, 4U)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(src, dst, 4U); + TEST_ASSERT_TRUE(ring_buffer_is_empty(&rb)); +} + +void test_write_partial_when_nearly_full(void) +{ + /* Заполнить 6 из 8 байт */ + uint8_t fill[6] = { 0 }; + ring_buffer_write(&rb, fill, 6U); + TEST_ASSERT_EQUAL_size_t(2U, ring_buffer_free(&rb)); + + /* Попытка записать 5, должно записаться 2 */ + const uint8_t src[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE }; + size_t written = ring_buffer_write(&rb, src, 5U); + + TEST_ASSERT_EQUAL_size_t(2U, written); + TEST_ASSERT_TRUE(ring_buffer_is_full(&rb)); +} + +void test_read_partial_when_not_enough_data(void) +{ + ring_buffer_put(&rb, 0x11); + ring_buffer_put(&rb, 0x22); + + uint8_t dst[5] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + size_t n = ring_buffer_read(&rb, dst, 5U); + + /* Прочитано только то, что было */ + TEST_ASSERT_EQUAL_size_t(2U, n); + TEST_ASSERT_EQUAL_UINT8(0x11, dst[0]); + TEST_ASSERT_EQUAL_UINT8(0x22, dst[1]); + /* Остаток dst не тронут */ + TEST_ASSERT_EQUAL_UINT8(0xFF, dst[2]); +} + +void test_read_from_empty_returns_zero(void) +{ + uint8_t dst[4]; + TEST_ASSERT_EQUAL_size_t(0U, ring_buffer_read(&rb, dst, 4U)); +} + +/* ═══════════════════════════════════════════════════════════════════════ + * 4. Full / Empty — граничные состояния + * ═══════════════════════════════════════════════════════════════════════ */ + +void test_empty_after_init(void) +{ + TEST_ASSERT_TRUE(ring_buffer_is_empty(&rb)); + TEST_ASSERT_FALSE(ring_buffer_is_full(&rb)); + TEST_ASSERT_EQUAL_size_t(0U, ring_buffer_count(&rb)); + TEST_ASSERT_EQUAL_size_t(BUF_SIZE, ring_buffer_free(&rb)); +} + +void test_full_after_filling(void) +{ + for (uint8_t i = 0; i < BUF_SIZE; i++) + { + ring_buffer_put(&rb, i); + } + TEST_ASSERT_TRUE(ring_buffer_is_full(&rb)); + TEST_ASSERT_FALSE(ring_buffer_is_empty(&rb)); + TEST_ASSERT_EQUAL_size_t(BUF_SIZE, ring_buffer_count(&rb)); + TEST_ASSERT_EQUAL_size_t(0U, ring_buffer_free(&rb)); +} + +void test_not_full_after_one_read_from_full(void) +{ + for (uint8_t i = 0; i < BUF_SIZE; i++) + { + ring_buffer_put(&rb, i); + } + uint8_t out; + ring_buffer_get(&rb, &out); + + TEST_ASSERT_FALSE(ring_buffer_is_full(&rb)); + TEST_ASSERT_EQUAL_size_t(BUF_SIZE - 1U, ring_buffer_count(&rb)); + TEST_ASSERT_EQUAL_size_t(1U, ring_buffer_free(&rb)); +} + +void test_empty_after_draining_full_buffer(void) +{ + for (uint8_t i = 0; i < BUF_SIZE; i++) + { + ring_buffer_put(&rb, i); + } + uint8_t out; + for (size_t i = 0; i < BUF_SIZE; i++) + { + ring_buffer_get(&rb, &out); + } + TEST_ASSERT_TRUE(ring_buffer_is_empty(&rb)); +} + +/* ═══════════════════════════════════════════════════════════════════════ + * 5. Wraparound — корректность при переполнении индексов + * + * Это самые важные тесты: именно здесь обычно живут баги. + * Цикл: заполнить → слить → заполнить снова — индексы пройдут через + * границу массива, маска & должна корректно их завернуть. + * ═══════════════════════════════════════════════════════════════════════ */ + +void test_wraparound_single_bytes(void) +{ + uint8_t out; + + /* Несколько полных циклов fill→drain, чтобы гарантированно + * пройти wraparound несколько раз. */ + for (int cycle = 0; cycle < 4; cycle++) + { + for (uint8_t i = 0; i < BUF_SIZE; i++) + { + TEST_ASSERT_TRUE(ring_buffer_put(&rb, i)); + } + for (uint8_t i = 0; i < BUF_SIZE; i++) + { + TEST_ASSERT_TRUE(ring_buffer_get(&rb, &out)); + TEST_ASSERT_EQUAL_UINT8(i, out); + } + TEST_ASSERT_TRUE(ring_buffer_is_empty(&rb)); + } +} + +void test_wraparound_partial_overlap(void) +{ + uint8_t out; + + /* Сдвинуть индексы к краю массива: записать 6, прочитать 6. */ + for (uint8_t i = 0; i < 6U; i++) + ring_buffer_put(&rb, i); + for (uint8_t i = 0; i < 6U; i++) + ring_buffer_get(&rb, &out); + + /* Теперь tail≈6, head≈6. Записать 5 — запись перейдёт через край. */ + const uint8_t src[] = { 0xAA, 0xBB, 0xCC, 0xDD, 0xEE }; + TEST_ASSERT_EQUAL_size_t(5U, ring_buffer_write(&rb, src, 5U)); + + /* Прочитать обратно — порядок должен сохраниться. */ + uint8_t dst[5]; + TEST_ASSERT_EQUAL_size_t(5U, ring_buffer_read(&rb, dst, 5U)); + TEST_ASSERT_EQUAL_UINT8_ARRAY(src, dst, 5U); +} + +void test_wraparound_preserves_data_integrity(void) +{ + /* Заполнить буфер контрольными значениями, слить наполовину, + * дозаписать новые данные — проверить весь порядок. */ + for (uint8_t i = 0; i < BUF_SIZE; i++) + { + ring_buffer_put(&rb, i * 10U); /* 0, 10, 20, 30, 40, 50, 60, 70 */ + } + + uint8_t out; + /* Слить первые 4 */ + for (uint8_t i = 0; i < 4U; i++) + { + ring_buffer_get(&rb, &out); + TEST_ASSERT_EQUAL_UINT8(i * 10U, out); + } + + /* Дозаписать 4 новых значения */ + for (uint8_t i = 0; i < 4U; i++) + { + ring_buffer_put(&rb, 0xA0U + i); + } + + /* Прочитать оставшиеся старые 4 */ + for (uint8_t i = 4U; i < BUF_SIZE; i++) + { + ring_buffer_get(&rb, &out); + TEST_ASSERT_EQUAL_UINT8(i * 10U, out); + } + + /* Прочитать новые 4 */ + for (uint8_t i = 0; i < 4U; i++) + { + ring_buffer_get(&rb, &out); + TEST_ASSERT_EQUAL_UINT8(0xA0U + i, out); + } + + TEST_ASSERT_TRUE(ring_buffer_is_empty(&rb)); +} + +/* ═══════════════════════════════════════════════════════════════════════ + * 6. SPSC simulation — имитация ISR-producer / task-consumer + * + * В реальном коде ISR пишет байты, задача читает. + * Здесь симулируем это в один поток, но чередуя операции — + * проверяем, что счётчики не расходятся при долгой работе. + * ═══════════════════════════════════════════════════════════════════════ */ + +void test_spsc_interleaved_puts_and_gets(void) +{ + uint8_t out; + uint8_t expected = 0U; + uint8_t next_in = 0U; + + /* 200 итераций: ISR кладёт 3, задача забирает 2. + * Буфер постепенно заполняется, потом стабилизируется у верхней границы. + * Главное — порядок данных сохранён. */ + for (int i = 0; i < 200; i++) + { + /* "ISR": положить до 3 байт если есть место */ + for (int p = 0; p < 3; p++) + { + if (!ring_buffer_is_full(&rb)) + { + ring_buffer_put(&rb, next_in++); + } + } + /* "Task": забрать до 2 байт */ + for (int c = 0; c < 2; c++) + { + if (!ring_buffer_is_empty(&rb)) + { + ring_buffer_get(&rb, &out); + TEST_ASSERT_EQUAL_UINT8(expected++, out); + } + } + } +} + +/* ═══════════════════════════════════════════════════════════════════════ + * 7. Reset + * ═══════════════════════════════════════════════════════════════════════ */ + +void test_reset_clears_data(void) +{ + ring_buffer_put(&rb, 0xDE); + ring_buffer_put(&rb, 0xAD); + TEST_ASSERT_EQUAL_size_t(2U, ring_buffer_count(&rb)); + + ring_buffer_reset(&rb); + + TEST_ASSERT_TRUE(ring_buffer_is_empty(&rb)); + TEST_ASSERT_EQUAL_size_t(0U, ring_buffer_count(&rb)); + TEST_ASSERT_EQUAL_size_t(BUF_SIZE, ring_buffer_free(&rb)); +} + +void test_reset_allows_reuse(void) +{ + /* Заполнить, сбросить, снова использовать — не должно быть артефактов. */ + for (uint8_t i = 0; i < BUF_SIZE; i++) + ring_buffer_put(&rb, i); + ring_buffer_reset(&rb); + + ring_buffer_put(&rb, 0x42); + uint8_t out = 0; + ring_buffer_get(&rb, &out); + TEST_ASSERT_EQUAL_UINT8(0x42, out); + TEST_ASSERT_TRUE(ring_buffer_is_empty(&rb)); +} + +/* ── Runner ──────────────────────────────────────────────────────────── */ + +int main(void) +{ + UNITY_BEGIN(); + + /* Init */ + RUN_TEST(test_init_valid); + RUN_TEST(test_init_rejects_null_rb); + RUN_TEST(test_init_rejects_null_buf); + RUN_TEST(test_init_rejects_zero_size); + RUN_TEST(test_init_rejects_non_power_of_two); + RUN_TEST(test_init_accepts_all_valid_powers_of_two); + + /* Put / Get */ + RUN_TEST(test_put_get_single_byte); + RUN_TEST(test_get_from_empty_returns_false); + RUN_TEST(test_put_returns_false_when_full); + RUN_TEST(test_fifo_ordering); + + /* Write / Read */ + RUN_TEST(test_write_read_full_block); + RUN_TEST(test_write_partial_when_nearly_full); + RUN_TEST(test_read_partial_when_not_enough_data); + RUN_TEST(test_read_from_empty_returns_zero); + + /* Full / Empty */ + RUN_TEST(test_empty_after_init); + RUN_TEST(test_full_after_filling); + RUN_TEST(test_not_full_after_one_read_from_full); + RUN_TEST(test_empty_after_draining_full_buffer); + + /* Wraparound */ + RUN_TEST(test_wraparound_single_bytes); + RUN_TEST(test_wraparound_partial_overlap); + RUN_TEST(test_wraparound_preserves_data_integrity); + + /* SPSC simulation */ + RUN_TEST(test_spsc_interleaved_puts_and_gets); + + /* Reset */ + RUN_TEST(test_reset_clears_data); + RUN_TEST(test_reset_allows_reuse); + + return UNITY_END(); +} \ No newline at end of file diff --git a/tests/host/timeout/test_timeout_pattern.c b/tests/host/timeout/test_timeout_pattern.c new file mode 100644 index 0000000..0ebf72f --- /dev/null +++ b/tests/host/timeout/test_timeout_pattern.c @@ -0,0 +1,222 @@ +/** + * @file test_timeout_pattern.c + * @brief Тесты паттерна таймаута (bsp_tick_get_ms() - start) < timeout. + * + * Паттерн используется в bsp_uart_read(), bsp_delay() и других + * BSP-модулях. Тестируем математику изолированно — без железа и моков. + * + * Ключевое свойство: беззнаковая арифметика uint32_t корректна + * при wraparound счётчика (переполнение через UINT32_MAX). + */ + +#include "unity.h" + +#include + +void setUp(void) +{ +} +void tearDown(void) +{ +} + +/* ── Вспомогательный макрос — сам паттерн ───────────────────────────── */ + +/** + * Возвращает 1 если таймаут истёк. + * now и start — uint32_t (как bsp_tick_get_ms()). + */ +#define TIMEOUT_EXPIRED(now, start, timeout_ms) \ + (((uint32_t) (now) - (uint32_t) (start)) >= (uint32_t) (timeout_ms)) + +/* ═══════════════════════════════════════════════════════════════════════ + * 1. Нормальные случаи (без wraparound) + * ═══════════════════════════════════════════════════════════════════════ */ + +void test_not_expired_elapsed_less_than_timeout(void) +{ + uint32_t start = 1000U; + uint32_t now = 1099U; + uint32_t timeout = 100U; + + TEST_ASSERT_FALSE(TIMEOUT_EXPIRED(now, start, timeout)); +} + +void test_expired_elapsed_equals_timeout(void) +{ + /* Граничное условие: elapsed == timeout → истёк. */ + uint32_t start = 1000U; + uint32_t now = 1100U; + uint32_t timeout = 100U; + + TEST_ASSERT_TRUE(TIMEOUT_EXPIRED(now, start, timeout)); +} + +void test_expired_elapsed_greater_than_timeout(void) +{ + uint32_t start = 1000U; + uint32_t now = 1200U; + uint32_t timeout = 100U; + + TEST_ASSERT_TRUE(TIMEOUT_EXPIRED(now, start, timeout)); +} + +void test_not_expired_at_zero_elapsed(void) +{ + /* now == start: ничего не прошло. */ + uint32_t start = 5000U; + uint32_t now = 5000U; + uint32_t timeout = 1U; + + TEST_ASSERT_FALSE(TIMEOUT_EXPIRED(now, start, timeout)); +} + +void test_zero_timeout_always_expired(void) +{ + /* + * timeout == 0: 0 >= 0 всегда true. + * Это ожидаемое поведение — вызывающий код трактует 0 + * как «не ждать вообще». + */ + uint32_t start = 1000U; + uint32_t now = 1000U; + uint32_t timeout = 0U; + + TEST_ASSERT_TRUE(TIMEOUT_EXPIRED(now, start, timeout)); +} + +/* ═══════════════════════════════════════════════════════════════════════ + * 2. Wraparound — счётчик переполнился через UINT32_MAX + * ═══════════════════════════════════════════════════════════════════════ */ + +void test_wraparound_not_expired(void) +{ + /* + * start близко к UINT32_MAX, now уже за нулём. + * elapsed = (50 - (UINT32_MAX - 49)) = 100 в беззнаковой арифметике. + * timeout = 150 → ещё не истёк. + */ + uint32_t start = UINT32_MAX - 49U; /* 4294967246 */ + uint32_t now = 50U; + uint32_t timeout = 150U; + + /* elapsed = now - start = 50 - 4294967246 = 100 (mod 2^32) */ + TEST_ASSERT_EQUAL_UINT32(100U, now - start); + TEST_ASSERT_FALSE(TIMEOUT_EXPIRED(now, start, timeout)); +} + +void test_wraparound_exactly_at_timeout(void) +{ + uint32_t start = UINT32_MAX - 49U; + uint32_t now = 50U; + uint32_t timeout = 100U; /* elapsed == timeout → истёк */ + + TEST_ASSERT_TRUE(TIMEOUT_EXPIRED(now, start, timeout)); +} + +void test_wraparound_expired(void) +{ + uint32_t start = UINT32_MAX - 49U; + uint32_t now = 100U; + uint32_t timeout = 100U; + + /* elapsed = 150 >= 100 → истёк */ + TEST_ASSERT_TRUE(TIMEOUT_EXPIRED(now, start, timeout)); +} + +void test_wraparound_start_at_max(void) +{ + /* start = UINT32_MAX, now = 0: elapsed = 1 */ + uint32_t start = UINT32_MAX; + uint32_t now = 0U; + uint32_t timeout = 1U; + + TEST_ASSERT_EQUAL_UINT32(1U, now - start); + TEST_ASSERT_TRUE(TIMEOUT_EXPIRED(now, start, timeout)); +} + +void test_wraparound_start_at_max_not_expired(void) +{ + /* start = UINT32_MAX, now = 0: elapsed = 1, timeout = 2 → не истёк */ + uint32_t start = UINT32_MAX; + uint32_t now = 0U; + uint32_t timeout = 2U; + + TEST_ASSERT_FALSE(TIMEOUT_EXPIRED(now, start, timeout)); +} + +/* ═══════════════════════════════════════════════════════════════════════ + * 3. Почему альтернативный паттерн НЕВЕРЕН + * + * Документируем антипаттерн: (now >= start + timeout). + * При wraparound start + timeout переполняется и сравнение ломается. + * Эти тесты показывают конкретные случаи поломки. + * ═══════════════════════════════════════════════════════════════════════ */ + +void test_antipattern_breaks_at_wraparound(void) +{ + /* + * Антипаттерн: now >= (start + timeout) + * start = UINT32_MAX - 49, timeout = 100 + * start + timeout = UINT32_MAX - 49 + 100 = 50 (переполнение!) + * + * now = 80: реальный elapsed = 130 >= 100 → должно быть expired. + * Но антипаттерн: 80 >= 50 → true (случайно верно здесь) + * + * now = 30: реальный elapsed = 80 < 100 → не истёк. + * Антипаттерн: 30 >= 50 → false (верно, но по случайности) + * + * now = 40: реальный elapsed = 90 < 100 → не истёк. + * Антипаттерн: 40 >= 50 → false (верно) + * + * now = 60: реальный elapsed = 110 >= 100 → истёк. + * Антипаттерн: 60 >= 50 → true (верно) + * + * Сложный случай — start близко к MAX, timeout большой: + * start = UINT32_MAX - 10, timeout = UINT32_MAX - 100 + * start + timeout переполняется в очень маленькое число → + * антипаттерн скажет "истёк" почти сразу. + */ + + uint32_t start = UINT32_MAX - 10U; + uint32_t timeout = 1000U; + uint32_t now = start + 5U; /* elapsed = 5, далеко до timeout */ + + /* Правильный паттерн: не истёк (5 < 1000) */ + TEST_ASSERT_FALSE(TIMEOUT_EXPIRED(now, start, timeout)); + + /* Антипаттерн: start + timeout переполнился → (start+timeout) маленькое, + * now > (start+timeout) → антипаттерн скажет "истёк" — НЕВЕРНО. */ + uint32_t wrong_deadline = start + timeout; /* wraparound! */ + int antipattern_result = (now >= wrong_deadline); + + /* Демонстрируем что антипаттерн даёт неверный результат: */ + TEST_ASSERT_TRUE(antipattern_result); /* антипаттерн говорит "истёк" */ + TEST_ASSERT_FALSE(TIMEOUT_EXPIRED(now, start, timeout)); /* правильно: нет */ +} + +/* ── Runner ──────────────────────────────────────────────────────────── */ + +int main(void) +{ + UNITY_BEGIN(); + + /* Нормальные случаи */ + RUN_TEST(test_not_expired_elapsed_less_than_timeout); + RUN_TEST(test_expired_elapsed_equals_timeout); + RUN_TEST(test_expired_elapsed_greater_than_timeout); + RUN_TEST(test_not_expired_at_zero_elapsed); + RUN_TEST(test_zero_timeout_always_expired); + + /* Wraparound */ + RUN_TEST(test_wraparound_not_expired); + RUN_TEST(test_wraparound_exactly_at_timeout); + RUN_TEST(test_wraparound_expired); + RUN_TEST(test_wraparound_start_at_max); + RUN_TEST(test_wraparound_start_at_max_not_expired); + + /* Антипаттерн */ + RUN_TEST(test_antipattern_breaks_at_wraparound); + + return UNITY_END(); +} \ No newline at end of file diff --git a/tools/README.md b/tools/host/README.md similarity index 100% rename from tools/README.md rename to tools/host/README.md diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt new file mode 100644 index 0000000..0293854 --- /dev/null +++ b/utils/CMakeLists.txt @@ -0,0 +1,14 @@ +# utils/CMakeLists.txt +# +# Платформо-независимые утилиты. Не зависят от SDK, BSP или железа — +# компилируются на хосте и таргете. +# +# Правило включения: код попадает сюда только если выполняются ОБА условия: 1. +# Не зависит от конкретного железа (нет fsl_*, CMSIS, FreeRTOS). 2. Используется +# более чем в одном месте проекта. + +add_library(utils STATIC ring_buffer/ring_buffer.c) + +# PUBLIC: любой таргет, слинкованный с utils, автоматически получает корень +# utils/ в include path → #include "ring_buffer/ring_buffer.h" +target_include_directories(utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/utils/README.md b/utils/README.md new file mode 100644 index 0000000..56ad94d --- /dev/null +++ b/utils/README.md @@ -0,0 +1,48 @@ +# utils + +Платформо-независимые утилиты проекта. + +**Правило включения** — код попадает сюда только если выполняются оба условия: + +- не зависит от железа (нет `fsl_*`, CMSIS, FreeRTOS, BSP); +- используется более чем в одном месте проекта. + +Все модули компилируются на хосте (host-тесты) и на таргете без изменений. + +--- + +## Модули + +### `ring_buffer` + +Кольцевой буфер байт — SPSC (single-producer / single-consumer), lock-free. + +**Типичное использование:** ISR пишет принятые байты, задача или основной цикл +читает. Не требует отключения прерываний при условии единственного producer и +единственного consumer. + +| Параметр | Значение | +|---|---| +| Элемент | 1 байт (`uint8_t`) | +| Ёмкость | любая степень двойки, задаётся при `ring_buffer_init` | +| Thread-safety | SPSC без блокировок; multi-producer/consumer — только с внешней синхронизацией | +| Зависимости | ``, ``, `` | + +```c +#include "ring_buffer/ring_buffer.h" + +static uint8_t storage[256]; +static ring_buffer_t rb; + +// Инициализация (размер — степень двойки) +ring_buffer_init(&rb, storage, sizeof(storage)); + +// Запись (например, из ISR) +ring_buffer_put(&rb, byte); + +// Чтение (например, из задачи) +uint8_t b; +if (ring_buffer_get(&rb, &b)) { /* обработать b */ } +``` + +Тесты: `tests/host/test_ring_buffer.c` (24 теста, включая wraparound и SPSC-симуляцию). diff --git a/utils/ring_buffer/ring_buffer.c b/utils/ring_buffer/ring_buffer.c new file mode 100644 index 0000000..2c2d77f --- /dev/null +++ b/utils/ring_buffer/ring_buffer.c @@ -0,0 +1,143 @@ +/** + * @file ring_buffer.c + * @brief Реализация кольцевого буфера (SPSC, lock-free). + * + * Ключевой инвариант SPSC-безопасности: + * - producer читает head (чужой индекс) только для проверки is_full. + * - consumer читает tail (чужой индекс) только для проверки is_empty/count. + * - Каждый индекс пишет только его владелец. + * - volatile гарантирует, что компилятор не кэширует чужой индекс. + * - Беззнаковый wraparound size_t корректен: (0 - 1) == SIZE_MAX, + * арифметика modulo 2^N работает без явной обёртки. + */ + +#include "ring_buffer.h" + +#include + +/* ── Вспомогательная функция ─────────────────────────────────────────── */ + +/** Проверить, является ли n степенью двойки (и n > 0). */ +static inline bool is_power_of_two(size_t n) +{ + return (n > 0U) && ((n & (n - 1U)) == 0U); +} + +/* ── Инициализация ───────────────────────────────────────────────────── */ + +bool ring_buffer_init(ring_buffer_desc_t *p_desc, uint8_t *p_buf, size_t size) +{ + if (p_desc == NULL || p_buf == NULL || !is_power_of_two(size)) + { + return false; + } + + p_desc->buf = p_buf; + p_desc->size = size; + p_desc->mask = size - 1U; + p_desc->tail = 0U; + p_desc->head = 0U; + + return true; +} + +void ring_buffer_reset(ring_buffer_desc_t *p_desc) +{ + p_desc->tail = 0U; + p_desc->head = 0U; +} + +/* ── Состояние ───────────────────────────────────────────────────────── */ + +bool ring_buffer_is_empty(const ring_buffer_desc_t *p_desc) +{ + return p_desc->tail == p_desc->head; +} + +bool ring_buffer_is_full(const ring_buffer_desc_t *p_desc) +{ + return (p_desc->tail - p_desc->head) == p_desc->size; +} + +size_t ring_buffer_count(const ring_buffer_desc_t *p_desc) +{ + /* Беззнаковое вычитание: корректно при любом wraparound. */ + return p_desc->tail - p_desc->head; +} + +size_t ring_buffer_free(const ring_buffer_desc_t *p_desc) +{ + return p_desc->size - (p_desc->tail - p_desc->head); +} + +/* ── Запись (producer) ───────────────────────────────────────────────── */ + +bool ring_buffer_put(ring_buffer_desc_t *p_desc, uint8_t byte) +{ + if (ring_buffer_is_full(p_desc)) + { + return false; + } + + /* + * Порядок важен для SPSC: + * 1. Записать данные в ячейку. + * 2. Только потом продвинуть tail — это делает байт видимым consumer-у. + * + * На Cortex-M7 (strongly-ordered memory model) барьер не нужен. + * На weakly-ordered архитектурах (ARM64, RISC-V) нужен store-release. + * Если понадобится портируемость — добавить __atomic_store_n. + */ + p_desc->buf[p_desc->tail & p_desc->mask] = byte; + p_desc->tail++; + + return true; +} + +size_t ring_buffer_write(ring_buffer_desc_t *p_desc, const uint8_t *p_data, size_t len) +{ + size_t written = 0U; + + while (written < len && !ring_buffer_is_full(p_desc)) + { + p_desc->buf[p_desc->tail & p_desc->mask] = p_data[written]; + p_desc->tail++; + written++; + } + + return written; +} + +/* ── Чтение (consumer) ───────────────────────────────────────────────── */ + +bool ring_buffer_get(ring_buffer_desc_t *p_desc, uint8_t *p_byte) +{ + if (ring_buffer_is_empty(p_desc)) + { + return false; + } + + /* + * Симметрично put(): + * 1. Прочитать данные из ячейки. + * 2. Продвинуть head — освобождает место для producer-а. + */ + *p_byte = p_desc->buf[p_desc->head & p_desc->mask]; + p_desc->head++; + + return true; +} + +size_t ring_buffer_read(ring_buffer_desc_t *p_desc, uint8_t *p_data, size_t len) +{ + size_t read = 0U; + + while (read < len && !ring_buffer_is_empty(p_desc)) + { + p_data[read] = p_desc->buf[p_desc->head & p_desc->mask]; + p_desc->head++; + read++; + } + + return read; +} \ No newline at end of file diff --git a/utils/ring_buffer/ring_buffer.h b/utils/ring_buffer/ring_buffer.h new file mode 100644 index 0000000..88b6876 --- /dev/null +++ b/utils/ring_buffer/ring_buffer.h @@ -0,0 +1,105 @@ +/** + * @file ring_buffer.h + * @brief Кольцевой буфер — SPSC, lock-free для Cortex-M и host. + * + * Конвенция индексов: + * tail — индекс следующей ЗАПИСИ (двигает producer / ISR) + * head — индекс следующего ЧТЕНИЯ (двигает consumer / задача) + * + * Запись: buf[tail & mask] = byte; tail++; + * Чтение: byte = buf[head & mask]; head++; + * Счётчик: tail - head (беззнаковая арифметика, wraparound корректен) + * + * запись чтение + * ↓ ↓ + * ... [ ][ ][tail][D][D][D][D][D][head][ ][ ] ... + * ←───── данные ──────→ + * + * Гарантии потокобезопасности: + * - Ровно один writer и один reader (SPSC). + * - Запись и чтение не требуют отключения прерываний. + * - head и tail объявлены volatile — компилятор не кэширует в регистрах. + * - На Cortex-M запись/чтение size_t (32 бит) атомарны по архитектуре. + * + * Ограничения: + * - size ОБЯЗАНА быть степенью двойки (проверяется в ring_buffer_init). + * - Элемент — ровно 1 байт. Для 9-bit UART используй uint8_t[2] снаружи. + * - Multi-producer/consumer — только с внешней синхронизацией. + * + * Платформо-независим: зависит только от , , . + */ +#ifndef RING_BUFFER_H_ +#define RING_BUFFER_H_ +#include +#include +#include + +typedef struct +{ + uint8_t *buf; /**< Указатель на внешний массив данных. */ + size_t size; /**< Размер массива (степень двойки). */ + size_t mask; /**< size - 1, для быстрого & вместо %. */ + volatile size_t head; /**< Индекс следующего чтения (consumer). */ + volatile size_t tail; /**< Индекс следующей записи (producer). */ +} ring_buffer_desc_t; + +/** + * @brief Инициализировать дескриптор. + * + * @param p_desc Дескриптор (не NULL). + * @param p_buf Внешний массив (не NULL, размер = @p size байт). + * @param size Размер массива — ОБЯЗАТЕЛЬНО степень двойки. + * @return true при успехе, false если size не степень двойки. + */ +bool ring_buffer_init(ring_buffer_desc_t *p_desc, uint8_t *p_buf, size_t size); + +/** + * @brief Сбросить буфер в пустое состояние. + * + * @warning Не thread-safe. Вызов только когда ни producer, + * ни consumer не активны (например, при переинициализации). + */ +void ring_buffer_reset(ring_buffer_desc_t *p_desc); + +/** + * @brief Положить один байт. + * @return true если байт записан, false если буфер полон. + */ +bool ring_buffer_put(ring_buffer_desc_t *p_desc, uint8_t byte); + +/* ── Запись (producer side) ──────────────────────────────────────────── */ +/** + * @brief Положить блок байт. + * @return Количество реально записанных байт (< len если буфер заполнился). + */ +size_t ring_buffer_write(ring_buffer_desc_t *p_desc, const uint8_t *p_data, size_t len); + +/* ── Чтение (consumer side) ──────────────────────────────────────────── */ + +/** + * @brief Прочитать один байт. + * @return true если байт получен, false если буфер пуст. + */ +bool ring_buffer_get(ring_buffer_desc_t *p_desc, uint8_t *p_byte); + +/** + * @brief Прочитать блок байт. + * @return Количество реально прочитанных байт. + */ +size_t ring_buffer_read(ring_buffer_desc_t *p_desc, uint8_t *p_data, size_t len); + +/* ── Состояние ───────────────────────────────────────────────────────── */ + +/** @return true если буфер пуст (head == tail). */ +bool ring_buffer_is_empty(const ring_buffer_desc_t *p_desc); + +/** @return true если буфер полон (tail - head == size). */ +bool ring_buffer_is_full(const ring_buffer_desc_t *p_desc); + +/** @return Количество байт, доступных для чтения. */ +size_t ring_buffer_count(const ring_buffer_desc_t *p_desc); + +/** @return Количество байт, доступных для записи. */ +size_t ring_buffer_free(const ring_buffer_desc_t *p_desc); + +#endif //RING_BUFFER_H_ \ No newline at end of file