- добавлен модуль и тесты bsp/tick, tools/ring_buffer
This commit is contained in:
Dmitry Akimov 2026-03-16 16:53:16 +03:00
parent 72045dabb3
commit 96be9ce023
24 changed files with 1511 additions and 125 deletions

View file

@ -12,25 +12,22 @@
"extensions": [ "extensions": [
"actboy168.tasks", "actboy168.tasks",
"bierner.markdown-preview-github-styles", "bierner.markdown-preview-github-styles",
"brobeson.ctest-lab",
"cheshirekow.cmake-format", "cheshirekow.cmake-format",
"codeium.codeium",
"cschlosser.doxdocgen", "cschlosser.doxdocgen",
"dan-c-underwood.arm", "dan-c-underwood.arm",
"davidanson.vscode-markdownlint", "davidanson.vscode-markdownlint",
"gruntfuggly.todo-tree", "gruntfuggly.todo-tree",
"kylinideteam.cmake-intellisence", "ms-vscode.cmake-tools",
"llvm-vs-code-extensions.vscode-clangd", "llvm-vs-code-extensions.vscode-clangd",
"mcu-debug.debug-tracker-vscode", "mcu-debug.debug-tracker-vscode",
"mcu-debug.memory-view", "mcu-debug.memory-view",
"mcu-debug.peripheral-viewer", "mcu-debug.peripheral-viewer",
"brobeson.ctest-lab",
"mcu-debug.rtos-views", "mcu-debug.rtos-views",
"ms-vscode.hexeditor", "ms-vscode.hexeditor",
"ms-vscode.makefile-tools", "nefrob.vscode-just-syntax",
"probe-rs.probe-rs-debugger",
"rioj7.command-variable", "rioj7.command-variable",
"trond-snekvik.gnu-mapfiles", "trond-snekvik.gnu-mapfiles",
"twxs.cmake",
"usernamehw.errorlens", "usernamehw.errorlens",
"wayou.vscode-todo-highlight", "wayou.vscode-todo-highlight",
"xaver.clang-format", "xaver.clang-format",

View file

@ -23,7 +23,7 @@ endif()
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
add_subdirectory(sdk) add_subdirectory(sdk)
add_subdirectory(bsp) add_subdirectory(bsp)
add_subdirectory(utils)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Сторонние и внутренние библиотеки # Сторонние и внутренние библиотеки
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View file

@ -128,7 +128,9 @@
"displayName": "Host Tests — Debug", "displayName": "Host Tests — Debug",
"configurePreset": "host-debug", "configurePreset": "host-debug",
"targets": [ "targets": [
"test_bsp_led" "test_bsp_led",
"test_ring_buffer",
"test_timeout_pattern"
] ]
}, },
{ {
@ -136,7 +138,9 @@
"displayName": "Host Tests — Release", "displayName": "Host Tests — Release",
"configurePreset": "host-release", "configurePreset": "host-release",
"targets": [ "targets": [
"test_bsp_led" "test_bsp_led",
"test_ring_buffer",
"test_timeout_pattern"
] ]
} }
], ],

View file

@ -12,6 +12,7 @@ add_library(
generated/startup/startup_MIMXRT1052.S) generated/startup/startup_MIMXRT1052.S)
add_subdirectory(led) add_subdirectory(led)
add_subdirectory(tick)
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки # Подавляем предупреждения при компиляции собственных .c файлов библиотеки
target_compile_options(bsp_board PRIVATE -w) target_compile_options(bsp_board PRIVATE -w)

View file

@ -4,7 +4,7 @@
/*! @brief The board name */ /*! @brief The board name */
#define BOARD_NAME "TFT4.3_UB_BOARD" #define BOARD_NAME "TFT4.3_UB_BOARD"
/*! //TODO: @brief The board name */ /*! @brief The board name */
void board_hw_init(void); void board_hw_init(void);
#endif /* _BOARD_H_ */ #endif /* _BOARD_H_ */

View file

@ -19,13 +19,13 @@
## API ## API
```c ```c
void led_init(void); // вызвать один раз после BOARD_InitPins() void bsp_led_init(void); // вызвать один раз после board_hw_init()
void led_on(led_id_t id); void bsp_led_on(led_id_t id);
void led_off(led_id_t id); void bsp_led_off(led_id_t id);
void led_toggle(led_id_t id); void bsp_led_toggle(led_id_t id);
void led_set(led_id_t id, bool on); void bsp_led_set(led_id_t id, bool on);
bool led_get(led_id_t id); bool bsp_led_get(led_id_t id);
``` ```
--- ---
@ -36,14 +36,14 @@ bool led_get(led_id_t id);
#include "bsp/led.h" #include "bsp/led.h"
// инициализация // инициализация
led_init(); bsp_led_init();
// heartbeat // heartbeat
led_toggle(LED_HEARTBEAT); bsp_led_toggle(LED_HEARTBEAT);
// прикладная индикация // прикладная индикация
led_on(LED_APP); // пакет принят / тест запущен bsp_led_on(LED_APP); // пакет принят / тест запущен
led_off(LED_APP); // сброс bsp_led_off(LED_APP); // сброс
``` ```
--- ---

View file

@ -1,4 +1,5 @@
#pragma once #ifndef BSP_LED_H_
#define BSP_LED_H_
/** /**
* @file led.h * @file led.h
@ -7,7 +8,7 @@
* LED_HEARTBEAT системный, мигает как признак жизни прошивки * LED_HEARTBEAT системный, мигает как признак жизни прошивки
* LED_APP прикладной, управляется из firmware по ситуации * LED_APP прикладной, управляется из firmware по ситуации
* *
* Пины сконфигурированы в generated/pin_mux. Этот хедер не знает * Пины сконфигурированы в generated/pin_mux.h. Этот хедер не знает
* ни про GPIO-порты, ни про NXP SDK. * ни про GPIO-порты, ни про NXP SDK.
*/ */
@ -25,25 +26,27 @@ typedef enum
/** /**
* @brief Инициализация обоих светодиодов. * @brief Инициализация обоих светодиодов.
* Вызвать один раз после BOARD_InitPins(). * Вызвать один раз после board_init_hw().
* После вызова оба LED выключены. * После вызова оба LED выключены.
*/ */
void led_init(void); void bsp_led_init(void);
/** @brief Включить светодиод. */ /** @brief Включить светодиод. */
void led_on(led_id_t led_id); void bsp_led_on(led_id_t led_id);
/** @brief Выключить светодиод. */ /** @brief Выключить светодиод. */
void led_off(led_id_t led_id); void bsp_led_off(led_id_t led_id);
/** @brief Переключить состояние светодиода. */ /** @brief Переключить состояние светодиода. */
void led_toggle(led_id_t led_id); void bsp_led_toggle(led_id_t led_id);
/** /**
* @brief Установить состояние светодиода явно. * @brief Установить состояние светодиода явно.
* @param on true включить, false выключить * @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 — горит). */ /** @brief Получить текущее состояние (true — горит). */
bool led_get(led_id_t led_id); bool bsp_led_get(led_id_t led_id);
#endif //BSP_LED_H_

View file

@ -47,19 +47,19 @@ static led_desc_t s_leds[] = {
/* ── Приватные хелперы ───────────────────────────────────────────────── */ /* ── Приватные хелперы ───────────────────────────────────────────────── */
/** Перевести логическое состояние в физический уровень GPIO (active LOW). */ /** Перевести логическое состояние в физический уровень 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; 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 ───────────────────────────────────────────────────── */ /* ── Публичный API ───────────────────────────────────────────────────── */
void led_init(void) void bsp_led_init(void)
{ {
gpio_pin_config_t cfg = { gpio_pin_config_t cfg = {
.direction = kGPIO_DigitalOutput, .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; 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; return s_leds[led_id].state;
} }

14
bsp/tick/CMakeLists.txt Normal file
View file

@ -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 — транзитивно
)

138
bsp/tick/README.md Normal file
View file

@ -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();
// ... действие ...
}
```

103
bsp/tick/include/bsp/tick.h Normal file
View file

@ -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 <stdint.h>
/* ── Инициализация ───────────────────────────────────────────────────── */
/**
* @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);

101
bsp/tick/src/tick.c Normal file
View file

@ -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 */
}
}

View file

@ -12,7 +12,7 @@ target_sources(firmware_test
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
target_link_libraries( target_link_libraries(
firmware_test firmware_test
PRIVATE bsp_board bsp_led PRIVATE bsp_board bsp_led bsp_tick
# Опциональные библиотеки # Опциональные библиотеки
lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED
) )

View file

@ -1,14 +1,19 @@
#include "board.h" #include "board.h"
#include "bsp/led.h" #include "bsp/led.h"
#include "bsp/tick.h"
int main(void) int main(void)
{ {
board_hw_init();
led_init();
led_on(LED_HEARTBEAT); board_hw_init();
led_off(LED_APP); bsp_led_init();
bsp_tick_init();
const uint16_t DELAY_MS = 1000;
while (1) while (1)
{ {
bsp_led_on(LED_HEARTBEAT);
bsp_delay(DELAY_MS);
bsp_led_off(LED_HEARTBEAT);
bsp_delay(DELAY_MS);
} }
} }

View file

@ -3,28 +3,34 @@
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Вспомогательная функция добавления host-теста. # Вспомогательная функция добавления host-теста.
# #
# Использование: add_host_test( NAME <имя теста> SOURCES <тест.c> # Использование: add_host_test( NAME <имя теста> SOURCES <тест.c> [<модуль.c>
# <тестируемый_модуль.c> ... MOCKS <путь к папке с stub-хедерами> # # ...] # исходники теста + тестируемый код INCLUDES <dir> [<dir> ...] #
# опционально ) # дополнительные include-пути (опц.) MOCKS <dir> [<dir> ...] # папки
# со stub-хедерами (опц.) )
# #
# Каждый тест — отдельный исполняемый файл. CTest запускает их все. # Каждый тест — отдельный исполняемый файл. CTest запускает их все.
#
# Соглашения: - lib_external подключается всегда (Unity + fff). - INCLUDES и
# MOCKS — явные: каждый тест сам декларирует свои зависимости. Нет скрытых
# глобальных include-путей, специфичных для одного модуля.
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
function(add_host_test) 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}) add_executable(${ARG_NAME} ${ARG_SOURCES})
target_link_libraries(${ARG_NAME} PRIVATE lib_external) 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) foreach(mock_dir IN LISTS ARG_MOCKS)
target_include_directories(${ARG_NAME} PRIVATE ${mock_dir}) target_include_directories(${ARG_NAME} PRIVATE ${mock_dir})
endforeach() endforeach()
# публичные хедеры BSP нужны для #include "bsp/led.h"
target_include_directories(${ARG_NAME}
PRIVATE ${PROJECT_SOURCE_DIR}/bsp/led/include)
add_test( add_test(
NAME ${ARG_NAME} NAME ${ARG_NAME}
COMMAND ${ARG_NAME} COMMAND ${ARG_NAME}
@ -44,8 +50,31 @@ add_host_test(
NAME NAME
test_bsp_led test_bsp_led
SOURCES SOURCES
test_bsp_led.c led/test_bsp_led.c
${PROJECT_SOURCE_DIR}/bsp/led/src/led.c # тестируемый модуль напрямую ${PROJECT_SOURCE_DIR}/bsp/led/src/led.c
INCLUDES
${PROJECT_SOURCE_DIR}/bsp/led/include # для #include "bsp/led.h"
MOCKS MOCKS
${BSP_MOCKS_DIR} # fsl_gpio.h, pin_mux.h, board.h ${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 — чистая математика, нет зависимостей
)

View file

@ -80,9 +80,9 @@ tests/host/
│ ├── fsl_lpuart.h │ ├── fsl_lpuart.h
│ ├── pin_mux.h # зеркало макросов пинов из generated/ │ ├── pin_mux.h # зеркало макросов пинов из generated/
│ └── board.h │ └── board.h
├── test_led.c # категория B — BSP-модуль ├── led/test_led.c # категория B — BSP-модуль
├── test_protocol.c # категория A — платформонезависимый ├── protocol/test_protocol.c # категория A — платформонезависимый
└── test_runner_logic.c # категория A └── runner/test_runner_logic.c # категория A
``` ```
`mocks/` подключается как include path с более высоким приоритетом чем `sdk/`. Компилятор найдёт `fsl_gpio.h` из `mocks/` раньше чем из SDK — `led.c` компилируется на хосте без единого изменения в BSP-коде. `mocks/` подключается как include path с более высоким приоритетом чем `sdk/`. Компилятор найдёт `fsl_gpio.h` из `mocks/` раньше чем из SDK — `led.c` компилируется на хосте без единого изменения в BSP-коде.

View file

@ -13,11 +13,11 @@ FAKE_VOID_FUNC(GPIO_PinWrite, GPIO_Type *, uint32_t, uint8_t);
/* ── Capture для gpio_pin_config_t ────────────────────────────────── /* ── Capture для gpio_pin_config_t ──────────────────────────────────
* fff хранит arg2_history как указатель но cfg живёт на стеке * fff хранит arg2_history как указатель но cfg живёт на стеке
* led_init() и становится dangling после возврата. * bsp_led_init() и становится dangling после возврата.
* (ASAN правильно детектирует это как stack-use-after-return) * (ASAN правильно детектирует это как stack-use-after-return)
* *
* Решение: custom_fake копирует структуру по значению в момент вызова, * Решение: custom_fake копирует структуру по значению в момент вызова,
* пока стек led_init() ещё жив. * пока стек bsp_led_init() ещё жив.
* */ * */
static gpio_pin_config_t s_captured_cfg[2]; static gpio_pin_config_t s_captured_cfg[2];
static int s_capture_idx = 0; static int s_capture_idx = 0;
@ -43,11 +43,11 @@ void setUp(void)
RESET_FAKE(GPIO_PinWrite); RESET_FAKE(GPIO_PinWrite);
FFF_RESET_HISTORY(); FFF_RESET_HISTORY();
/* подключаем capture до вызова led_init */ /* подключаем capture до вызова bsp_led_init */
s_capture_idx = 0; s_capture_idx = 0;
GPIO_PinInit_fake.custom_fake = GPIO_PinInit_capture; GPIO_PinInit_fake.custom_fake = GPIO_PinInit_capture;
led_init(); bsp_led_init();
} }
void tearDown(void) 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); 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[0].direction);
TEST_ASSERT_EQUAL(kGPIO_DigitalOutput, s_captured_cfg[1].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 выключен) */ /* active LOW — начальное состояние GPIO = 1 (LED выключен) */
TEST_ASSERT_EQUAL_UINT8(1U, s_captured_cfg[0].outputLogic); TEST_ASSERT_EQUAL_UINT8(1U, s_captured_cfg[0].outputLogic);
TEST_ASSERT_EQUAL_UINT8(1U, s_captured_cfg[1].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(bsp_led_get(LED_HEARTBEAT));
TEST_ASSERT_FALSE(led_get(LED_APP)); 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); bsp_led_on(LED_HEARTBEAT);
TEST_ASSERT_TRUE(led_get(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); bsp_led_on(LED_HEARTBEAT);
led_off(LED_HEARTBEAT); bsp_led_off(LED_HEARTBEAT);
TEST_ASSERT_FALSE(led_get(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 */ /* active LOW: включить LED = записать 0 в GPIO */
led_on(LED_HEARTBEAT); bsp_led_on(LED_HEARTBEAT);
TEST_ASSERT_EQUAL_UINT8(0U, GPIO_PinWrite_fake.arg2_val); 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 */ /* active LOW: выключить LED = записать 1 в GPIO */
led_off(LED_HEARTBEAT); bsp_led_off(LED_HEARTBEAT);
TEST_ASSERT_EQUAL_UINT8(1U, GPIO_PinWrite_fake.arg2_val); 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)); TEST_ASSERT_FALSE(bsp_led_get(LED_HEARTBEAT));
led_toggle(LED_HEARTBEAT); bsp_led_toggle(LED_HEARTBEAT);
TEST_ASSERT_TRUE(led_get(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); bsp_led_on(LED_HEARTBEAT);
led_toggle(LED_HEARTBEAT); bsp_led_toggle(LED_HEARTBEAT);
TEST_ASSERT_FALSE(led_get(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); bsp_led_toggle(LED_HEARTBEAT);
led_toggle(LED_HEARTBEAT); bsp_led_toggle(LED_HEARTBEAT);
TEST_ASSERT_FALSE(led_get(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); bsp_led_set(LED_APP, true);
TEST_ASSERT_TRUE(led_get(LED_APP)); 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); bsp_led_set(LED_APP, true);
led_set(LED_APP, false); bsp_led_set(LED_APP, false);
TEST_ASSERT_FALSE(led_get(LED_APP)); TEST_ASSERT_FALSE(bsp_led_get(LED_APP));
} }
/* ── Тест независимости LED друг от друга ───────────────────────────── */ /* ── Тест независимости LED друг от друга ───────────────────────────── */
void test_leds_are_independent(void) void test_leds_are_independent(void)
{ {
led_on(LED_HEARTBEAT); bsp_led_on(LED_HEARTBEAT);
TEST_ASSERT_TRUE(led_get(LED_HEARTBEAT)); TEST_ASSERT_TRUE(bsp_led_get(LED_HEARTBEAT));
TEST_ASSERT_FALSE(led_get(LED_APP)); TEST_ASSERT_FALSE(bsp_led_get(LED_APP));
led_on(LED_APP); bsp_led_on(LED_APP);
led_off(LED_HEARTBEAT); bsp_led_off(LED_HEARTBEAT);
TEST_ASSERT_FALSE(led_get(LED_HEARTBEAT)); TEST_ASSERT_FALSE(bsp_led_get(LED_HEARTBEAT));
TEST_ASSERT_TRUE(led_get(LED_APP)); TEST_ASSERT_TRUE(bsp_led_get(LED_APP));
} }
/* ── main ────────────────────────────────────────────────────────────── */ /* ── main ────────────────────────────────────────────────────────────── */
@ -168,22 +168,22 @@ int main(void)
{ {
UNITY_BEGIN(); UNITY_BEGIN();
RUN_TEST(test_led_init_calls_gpio_init_for_each_led); RUN_TEST(test_bsp_led_init_calls_gpio_init_for_each_led);
RUN_TEST(test_led_init_configures_as_output); RUN_TEST(test_bsp_led_init_configures_as_output);
RUN_TEST(test_led_init_output_logic_is_high); RUN_TEST(test_bsp_led_init_output_logic_is_high);
RUN_TEST(test_led_init_both_leds_off); RUN_TEST(test_bsp_led_init_both_leds_off);
RUN_TEST(test_led_on_sets_state_true); RUN_TEST(test_bsp_led_on_sets_state_true);
RUN_TEST(test_led_off_sets_state_false); RUN_TEST(test_bsp_led_off_sets_state_false);
RUN_TEST(test_led_on_writes_gpio_low); RUN_TEST(test_bsp_led_on_writes_gpio_low);
RUN_TEST(test_led_off_writes_gpio_high); RUN_TEST(test_bsp_led_off_writes_gpio_high);
RUN_TEST(test_led_toggle_off_to_on); RUN_TEST(test_bsp_led_toggle_off_to_on);
RUN_TEST(test_led_toggle_on_to_off); RUN_TEST(test_bsp_led_toggle_on_to_off);
RUN_TEST(test_led_toggle_twice_returns_to_initial); RUN_TEST(test_bsp_led_toggle_twice_returns_to_initial);
RUN_TEST(test_led_set_true_turns_on); RUN_TEST(test_bsp_led_set_true_turns_on);
RUN_TEST(test_led_set_false_turns_off); RUN_TEST(test_bsp_led_set_false_turns_off);
RUN_TEST(test_leds_are_independent); RUN_TEST(test_leds_are_independent);

View file

@ -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 <string.h>
/* ── Вспомогательные объекты ─────────────────────────────────────────── */
#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();
}

View file

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

14
utils/CMakeLists.txt Normal file
View file

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

48
utils/README.md Normal file
View file

@ -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 — только с внешней синхронизацией |
| Зависимости | `<stdint.h>`, `<stddef.h>`, `<stdbool.h>` |
```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-симуляцию).

View file

@ -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 <string.h>
/* ── Вспомогательная функция ─────────────────────────────────────────── */
/** Проверить, является ли 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;
}

View file

@ -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 только с внешней синхронизацией.
*
* Платформо-независим: зависит только от <stdint.h>, <stddef.h>, <stdbool.h>.
*/
#ifndef RING_BUFFER_H_
#define RING_BUFFER_H_
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
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_