From c4feaa8f453e84118f7422143bed86a6a8241a56 Mon Sep 17 00:00:00 2001 From: Ezra Maccabee Date: Mon, 23 Mar 2026 12:19:39 +0300 Subject: [PATCH] # 10 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - добавлен компонент util/log - логгер - написан адаптер UART для логгера: port/log - util/log покрыт юнит-тестами --- CMakeLists.txt | 4 +- CMakePresets.json | 2 + TODO.md | 152 ------------ firmware/test/CMakeLists.txt | 7 +- firmware/test/main.c | 21 +- port/CMakeLists.txt | 4 + port/README.md | 58 +++++ port/log/CMakeLists.txt | 24 ++ port/log/README.md | 51 ++++ port/log/include/port/log_uart.h | 40 ++++ port/log/src/log_mutex.c | 40 ++++ port/log/src/log_uart.c | 42 ++++ tests/host/CMakeLists.txt | 14 ++ tests/host/log/README.md | 0 tests/host/log/test_log.c | 396 +++++++++++++++++++++++++++++++ utils/CMakeLists.txt | 10 +- utils/README.md | 33 --- utils/log/log.c | 169 +++++++++++++ utils/log/log.h | 179 ++++++++++++++ utils/ring_buffer/README.md | 35 +++ 20 files changed, 1081 insertions(+), 200 deletions(-) delete mode 100644 TODO.md create mode 100644 port/CMakeLists.txt create mode 100644 port/README.md create mode 100644 port/log/CMakeLists.txt create mode 100644 port/log/README.md create mode 100644 port/log/include/port/log_uart.h create mode 100644 port/log/src/log_mutex.c create mode 100644 port/log/src/log_uart.c create mode 100644 tests/host/log/README.md create mode 100644 tests/host/log/test_log.c create mode 100644 utils/log/log.c create mode 100644 utils/log/log.h create mode 100644 utils/ring_buffer/README.md diff --git a/CMakeLists.txt b/CMakeLists.txt index f9ee4cd..ac5c7e8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,8 +32,10 @@ endif() # SDK и BSP — всегда подключаются, но содержат защиту от host сборки внутри # ----------------------------------------------------------------------------- add_subdirectory(sdk) -add_subdirectory(bsp) add_subdirectory(utils) +add_subdirectory(bsp) +add_subdirectory(port) + # ----------------------------------------------------------------------------- # Сторонние и внутренние библиотеки # ----------------------------------------------------------------------------- diff --git a/CMakePresets.json b/CMakePresets.json index b317d20..7105b85 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -140,6 +140,7 @@ "configurePreset": "host-debug", "targets": [ "test_bsp_led", + "test_log", "test_ring_buffer", "test_timeout_pattern", "uart_host_mock_example" @@ -151,6 +152,7 @@ "configurePreset": "host-release", "targets": [ "test_bsp_led", + "test_log", "test_ring_buffer", "test_timeout_pattern", "uart_host_mock_example" diff --git a/TODO.md b/TODO.md deleted file mode 100644 index a72332e..0000000 --- a/TODO.md +++ /dev/null @@ -1,152 +0,0 @@ -# Контекст проекта — состояние на текущий момент - -## Проект - -Монорепозиторий `tft_manufacture_test` для **MIMXRT1052CVJ5B** (Cortex-M7, NXP i.MX RT1052). Три firmware-проекта: `firmware/test` (входной контроль, bare-metal), `firmware/bootloader` (A/B обновление, bare-metal), `firmware/tft_app` (боевая прошивка, FreeRTOS). Сборка в devcontainer, операции с железом на хосте. - -Стек: CMake + Ninja + CMakePresets, `just` как task runner (модули `build.just`, `host.just`, `ci.just`), `uv` для Python-зависимостей, devcontainer (VSCode + arm-none-eabi-gcc + clangd). - ---- - -## Что сделано в этой сессии - -### 1. Отладка через SWD — настроена и работает - -**Архитектура:** -``` -Хост: pyocd gdbserver :3333 ← MCU-Link (CMSIS-DAP) ← SWD ← MIMXRT1052 -DevContainer: arm-none-eabi-gdb → host.docker.internal:3333 -VSCode: cortex-debug (servertype: external) -``` - -**Файлы:** -- `pyocd_debug.yaml` — конфиг pyOCD (`target_override: mimxrt1050_quadspi`, `frequency: 4000000`, `rtt.enabled: true`, `rtt.port: 4445`) -- `.vscode/launch.json` — три конфигурации cortex-debug -- `.vscode/tasks.json` — `preLaunchTask` для каждой конфигурации + `rtt:connect` -- `just/host.just` — рецепты `debug-server`, `flash-swd-*` - -**Параметры из `.env`:** -``` -GDB_PORT=3333 -PYOCD_TARGET=mimxrt1050_quadspi -PYOCD_FREQUENCY=4000000 -FCB_PATH=tools/host/dcd/w25q128_fdcb.bin -``` - -**Конфигурации launch.json:** -- `🐛 Debug: firmware_test` — `build/Debug/firmware_test.elf` -- `🐛 Debug: bootloader` — `build/Debug/bootloader.elf` -- `🐛 Debug: tft_app (FreeRTOS)` — `build/Debug/app.elf`, `"rtos": "FreeRTOS"` - -Все три: `loadFiles: []` (не перепрошивают), `runToEntryPoint: main`, SVD из `bsp/generated/startup/MIMXRT1052.xml`. - - - ---- - -### 2. Прошивка через SWD — настроена и работает - -**Проблема:** GDB `load` не работает для XIP-прошивок на IMXRT — Boot ROM при cold-start читает FCB по `0x60000000`, а ELF-секции кладутся без FCB. - -**Решение:** `tools/host/flash_swd.py` собирает итоговый образ: -``` -0x60000000 w25q128_fdcb.bin (512 байт) — FCB для W25Q128 Quad SPI -0x60000200 0xFF × 3584 байт — padding -0x60001000 *_hab.bin — IVT + DCD + код -``` - -Всё в одном 64KB-секторе — стирается и записывается за одну транзакцию. - - -**Рецепты в host.just:** -``` -just host::flash-swd-test-debug -just host::flash-swd-test-release -just host::flash-swd-bootloader-debug -just host::flash-swd-bootloader-release -just host::flash-swd-app-debug -just host::flash-swd-app-release -``` - -Скрипт читает конфигурацию из окружения (`PYOCD_TARGET`, `PYOCD_FREQUENCY`, `BUILD_DIR`, `FCB_PATH`). Относительные пути из `.env` автоматически разрешаются от `REPO_ROOT`. - -**Рабочий цикл А (прошивка уже в Flash):** -``` -just host::debug-server → VSCode: 🐛 Debug → F5 -``` - -**Рабочий цикл Б (прошить через SWD + отладить):** -``` -just build::hab-firmware-test-debug (в контейнере) -just host::flash-swd-test-debug (на хосте) -⚡ power cycle -just host::debug-server -VSCode: 🐛 Debug → F5 -``` - ---- - -### 3. RTT — решено отказаться, заменить на UART - -**Почему отказались от RTT:** -- `servertype: external` в cortex-debug официально не поддерживает RTT нативно -- Workaround через `postLaunchCommands` + `rtt_client.py` работает, но требует ручного запуска RTT-клиента после F5 -- Автозапуск через `preLaunchTask` зависает (клиент стартует до того как pyOCD открыл порт) -- Итоговое UX: два ручных действия вместо одного - -**Вывод:** RTT остаётся в проекте как возможность (код в `lib/SEGGER`, `SEGGER_RTT_ENABLED=ON` в Debug-пресете), но для логов использовать не будем. - ---- - -### 4. UART как канал логов — решение принято - -**BSP:** `bsp/uart_host` — `bsp_uart_host_write_str()` / `bsp_uart_host_write()`. - -TX — `LPUART_WriteBlocking` (blocking polling). Для логов это нормально: -- Bare-metal: нет проблем -- FreeRTOS: задача вытесняется по таймеру, но мьютекс нужен если несколько задач пишут лог - -**Ограничения bsp_uart_host для логгера:** -- Нельзя вызывать из ISR -- В FreeRTOS-контексте нужен мьютекс на уровне логгера (не в bsp_uart_host) - -**Мониторинг логов на хосте:** -``` -just host::uart-monitor -``` -Использует `python3 -m serial.tools.miniterm` (pyserial, кроссплатформенно — macOS/Linux/Windows). Порт и бод из `.env` (`HIL_VCOM_PORT`, `HIL_VCOM_BAUD`). - ---- - -## Следующий шаг — слой логгирования - -Планируется добавить логгер поверх `bsp_uart_host`. Кандидат — **log.c** (rxi, однофайловый, MIT). Нужно: - -1. Вендоринг `log.h` / `log.c` в `lib/log/` или `utils/log/` -2. CMake-таргет -3. Callback-адаптер → `bsp_uart_host_write_str()` -4. Мьютекс для FreeRTOS (в callback, не в bsp) -5. Запрет вызова из ISR (задокументировать) -6. Красивый вывод: уровень, файл, строка, ANSI-цвета - ---- - -## Структура файлов отладки (итог) - -``` -. -├── .env # GDB_PORT, PYOCD_*, RTT_PORT, FCB_PATH -├── .vscode/ -│ ├── launch.json # 3 конфигурации cortex-debug -│ └── tasks.json # build:*, rtt:connect, uart-monitor -├── pyocd_debug.yaml # target, frequency, rtt config -├── bsp/generated/startup/MIMXRT1052.xml # SVD -├── just/host.just # debug-server, flash-swd-*, uart-monitor -└── tools/ - ├── hil/ # uv-проект: pyocd, pyserial, pytest - └── host/ - ├── flash_swd.py # FCB+HAB → pyocd flash - ├── rtt_client.py # RTT TCP-клиент (retry-loop) - └── dcd/ - └── w25q128_fdcb.bin # FCB для W25Q128 Quad SPI -``` \ No newline at end of file diff --git a/firmware/test/CMakeLists.txt b/firmware/test/CMakeLists.txt index 99c9140..6e7f73e 100644 --- a/firmware/test/CMakeLists.txt +++ b/firmware/test/CMakeLists.txt @@ -15,7 +15,12 @@ target_compile_definitions(${TARGET_NAME} # ----------------------------------------------------------------------------- target_link_libraries( ${TARGET_NAME} - PRIVATE bsp_board bsp_led bsp_tick bsp_uart_host bsp_boot_xip + PRIVATE bsp_board + bsp_led + bsp_tick + bsp_uart_host + bsp_boot_xip + port_log_uart lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED ) diff --git a/firmware/test/main.c b/firmware/test/main.c index a6c801a..5e8697b 100644 --- a/firmware/test/main.c +++ b/firmware/test/main.c @@ -3,29 +3,28 @@ #include "bsp/led.h" #include "bsp/tick.h" #include "bsp/uart_host.h" +#include "log/log.h" +#include "port/log_uart.h" + +#include int main(void) { - const uint16_t DELAY_MS = 100; + const uint16_t DELAY_MS = 1000; const uint32_t UART_BAUDRATE = 115200; board_hw_init(); bsp_led_init(); bsp_tick_init(); + bsp_uart_host_init(UART_BAUDRATE); + log_uart_init(); - if (bsp_uart_host_init(UART_BAUDRATE) != BSP_OK) - { + LOG_I("BOOT", "firmware_test started, tick=%lu", (unsigned long) bsp_tick_get_ms()); + LOG_D("BOOT", "RX buffer ready, waiting for host..."); - while (1) - { - bsp_led_toggle(LED_HEARTBEAT); - bsp_delay(DELAY_MS); - } - } uint32_t cycle = 0; while (1) { - bsp_led_on(LED_HEARTBEAT); - bsp_uart_host_write_str("We are here!\n"); + LOG_D("CLI", "Running in the loop: '%d'", cycle % UINT32_MAX); bsp_delay(DELAY_MS); bsp_led_off(LED_HEARTBEAT); bsp_delay(DELAY_MS); diff --git a/port/CMakeLists.txt b/port/CMakeLists.txt new file mode 100644 index 0000000..a4a91bc --- /dev/null +++ b/port/CMakeLists.txt @@ -0,0 +1,4 @@ +if(BUILD_TESTS_HOST) + return() +endif() +add_subdirectory(log) diff --git a/port/README.md b/port/README.md new file mode 100644 index 0000000..f89547f --- /dev/null +++ b/port/README.md @@ -0,0 +1,58 @@ +# port/ — porting layer + +Glue-код между сторонними библиотеками (`lib/`) и платформой (`bsp/`). + +--- + +## Зачем нужен отдельный каталог + +В проекте три слоя кода с чёткими границами: + +```bash +lib/ сторонний код — ничего не знает о проекте +bsp/ железо — драйверы периферии MIMXRT1052 +utils/ платформонезависимые алгоритмы (ring_buffer, log и др.) +port/ ← glue: адаптирует lib/ и utils/ к конкретной платформе +firmware/ бизнес-логика — использует всё вышеперечисленное +``` + +Код попадает в `port/` если выполняются оба условия: + +1. Связывает платформонезависимую библиотеку/утилиту с конкретным BSP. +2. Сам по себе не является ни библиотекой, ни драйвером. + +Примеры: адаптер логгера к UART, diskio-реализация FatFS поверх bsp_sdio, +FreeRTOS heap и assert-хуки. + +Код НЕ попадает в `port/` если: + +- Не зависит от `bsp/` → идёт в `utils/` +- Является самостоятельным драйвером периферии → идёт в `bsp/` +- Это сторонняя библиотека без изменений → идёт в `lib/` + +--- + +## Структура + +```bash +port/ +├── CMakeLists.txt +├── README.md ← этот файл +└── log/ ← UART-адаптер для utils/log +# Планируется: +├── fatfs/ ← diskio поверх bsp_sdio / bsp_qspi +└── freertos/ ← heap_4.c, configASSERT, vApplicationHooks +``` + +--- + +## Соглашения + +**Именование таргетов:** `port_<что>_<транспорт>` — например `port_log_uart`, +`port_fatfs_sdio`. Позволяет иметь несколько адаптеров для одной библиотеки. + +**Include-путь:** `#include "port/<модуль>.h"` — публичные заголовки +всегда в `port/<модуль>/include/port/`. + +**Не компилируется для host-тестов:** `bsp/` недоступен на хосте, поэтому +`port/CMakeLists.txt` возвращает управление при `BUILD_TESTS_HOST=ON`. diff --git a/port/log/CMakeLists.txt b/port/log/CMakeLists.txt new file mode 100644 index 0000000..03b141e --- /dev/null +++ b/port/log/CMakeLists.txt @@ -0,0 +1,24 @@ +# ============================================================================= +# port_log_uart — UART-адаптер для utils_log. +# +# Связывает utils_log с bsp_uart_host: - регистрирует bsp_uart_host_write() как +# write callback - предоставляет strong-реализацию log_get_timestamp_ms() → +# bsp_tick_get_ms() +# +# Не компилируется для host-тестов (bsp_uart_host там недоступен). +# ============================================================================= + +if(BUILD_TESTS_HOST) + return() +endif() + +add_library(port_log_uart STATIC src/log_uart.c) + +target_include_directories(port_log_uart PUBLIC include/) + +target_link_libraries( + port_log_uart PUBLIC utils # несёт log.h и LOG_LEVEL транзитивно + bsp_uart_host bsp_tick) +# Явный include на случай если utils обрабатывается после port в корневом CMake. +# Дублирует транзитивный путь из utils — безвредно. +target_include_directories(port_log_uart PUBLIC ${CMAKE_SOURCE_DIR}/utils) diff --git a/port/log/README.md b/port/log/README.md new file mode 100644 index 0000000..97196fa --- /dev/null +++ b/port/log/README.md @@ -0,0 +1,51 @@ +# port/log — UART-адаптер логгера + +Подключает `utils/log` к `bsp_uart_host`. Является эталонной реализацией +транспортного адаптера — при добавлении нового транспорта (Flash, USB CDC) +создаётся аналогичный каталог `port/log_flash/` по той же схеме. + +--- + +## Использование + +```c +#include "port/log_uart.h" +#include "log/log.h" + +int main(void) +{ + board_hw_init(); + bsp_tick_init(); + bsp_uart_host_init(115200U); + + log_uart_init(); // регистрирует транспорт, после этого LOG_* работают + + LOG_I("BOOT", "Ready"); +} +``` + +Для **tft_app (FreeRTOS)** — добавить мьютекс до `log_uart_init()`: + +```c +log_mutex_init(); // создать FreeRTOS-семафор +log_uart_init(); // зарегистрировать транспорт +``` + +Реализация мьютекса: `firmware/tft_app/src/log_mutex.c`. + +--- + +## Что делает `log_uart_init()` + +1. Регистрирует `bsp_uart_host_write()` как write callback через `log_init()`. +2. Предоставляет strong-реализацию `log_get_timestamp_ms()` → `bsp_tick_get_ms()`. + +--- + +## CMake + +```cmake +target_link_libraries( PRIVATE port_log_uart) +``` + +Транзитивно подтягивает `utils` (содержит `log.h`) и `LOG_LEVEL`. diff --git a/port/log/include/port/log_uart.h b/port/log/include/port/log_uart.h new file mode 100644 index 0000000..489236a --- /dev/null +++ b/port/log/include/port/log_uart.h @@ -0,0 +1,40 @@ +/** + * @file port/log_uart.h + * @brief UART-адаптер логгера: регистрирует bsp_uart_host как транспорт. + * + * Инициализация в main(): + * @code + * bsp_tick_init(); + * bsp_uart_host_init(115200U); + * // В tft_app: log_mutex_init() здесь + * log_uart_init(); // регистрирует транспорт + timestamp + * @endcode + * + * При смене транспорта (Flash, USB CDC) — заменить этот файл на + * port/log_flash/log_flash.h и вызвать log_flash_init() вместо log_uart_init(). + * Код с LOG_I/LOG_D/... не меняется. + */ + +#ifndef PORT_LOG_UART_H +#define PORT_LOG_UART_H + +#ifdef __cplusplus +extern "C" +{ +#endif + + /** + * @brief Зарегистрировать UART как транспорт логгера. + * + * Вызывает log_init() с uart-callback и регистрирует + * log_get_timestamp_ms() → bsp_tick_get_ms(). + * + * Предусловия: bsp_uart_host_init() и bsp_tick_init() уже вызваны. + */ + void log_uart_init(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PORT_LOG_UART_H */ \ No newline at end of file diff --git a/port/log/src/log_mutex.c b/port/log/src/log_mutex.c new file mode 100644 index 0000000..cccdbc5 --- /dev/null +++ b/port/log/src/log_mutex.c @@ -0,0 +1,40 @@ +/** + * @file log_mutex.c + * @brief FreeRTOS-реализация мьютекса для логгера. + * + * Перекрывает weak-хуки из port/log/src/log.c strong-определениями. + * Компилируется только в tft_app (FreeRTOS), не в firmware/test и bootloader. + * + * Порядок вызовов в main() tft_app: + * 1. board_hw_init() + * 2. bsp_tick_init() + * 3. bsp_uart_host_init(115200U) + * 4. log_mutex_init() ← создаём семафор ДО log_port_init + * 5. log_port_init() + * + * @note Вызов LOG_* из ISR запрещён: xSemaphoreTake с portMAX_DELAY + * недопустим в ISR-контексте. + */ +#if 0 +#include "FreeRTOS.h" +#include "log/log.h" +#include "semphr.h" + +static SemaphoreHandle_t s_log_mutex; + +void log_mutex_init(void) +{ + s_log_mutex = xSemaphoreCreateMutex(); + configASSERT(s_log_mutex != NULL); +} + +void log_mutex_lock(void) +{ + xSemaphoreTake(s_log_mutex, portMAX_DELAY); +} + +void log_mutex_unlock(void) +{ + xSemaphoreGive(s_log_mutex); +} +#endif \ No newline at end of file diff --git a/port/log/src/log_uart.c b/port/log/src/log_uart.c new file mode 100644 index 0000000..4f2ff3f --- /dev/null +++ b/port/log/src/log_uart.c @@ -0,0 +1,42 @@ +/** + * @file log_uart.c + * @brief UART-адаптер логгера. + * + * Подключает utils_log к bsp_uart_host: + * - write callback → bsp_uart_host_write() + * - timestamp hook → bsp_tick_get_ms() + */ + +#include "port/log_uart.h" + +#include "bsp/tick.h" +#include "bsp/uart_host.h" +#include "log/log.h" + +/* -------------------------------------------------------------------------- */ +/* Strong-реализация weak-хука timestamp */ +/* -------------------------------------------------------------------------- */ + +uint32_t log_get_timestamp_ms(void) +{ + return bsp_tick_get_ms(); +} + +/* -------------------------------------------------------------------------- */ +/* Write callback */ +/* -------------------------------------------------------------------------- */ + +static void uart_write(const char *p_buf, size_t len, void *p_ctx) +{ + (void) p_ctx; + (void) bsp_uart_host_write((const uint8_t *) p_buf, len); +} + +/* -------------------------------------------------------------------------- */ +/* Публичный API */ +/* -------------------------------------------------------------------------- */ + +void log_uart_init(void) +{ + log_init(uart_write, NULL); +} \ No newline at end of file diff --git a/tests/host/CMakeLists.txt b/tests/host/CMakeLists.txt index c833f39..6a89711 100644 --- a/tests/host/CMakeLists.txt +++ b/tests/host/CMakeLists.txt @@ -71,6 +71,20 @@ add_host_test( ${PROJECT_SOURCE_DIR}/utils # для #include "ring_buffer/ring_buffer.h" ) +# ----------------------------------------------------------------------------- +# utils/log +# ----------------------------------------------------------------------------- +add_host_test( + NAME + test_log + SOURCES + log/test_log.c + ${PROJECT_SOURCE_DIR}/utils/log/log.c + INCLUDES + ${PROJECT_SOURCE_DIR}/utils # для "log/log.h" +) +# LOG_LEVEL=5 — все уровни активны, тесты уровней D и V не вырезаются +target_compile_definitions(test_log PRIVATE LOG_LEVEL=5) # ----------------------------------------------------------------------------- # Тест таймаут-паттерна, используемого в bsp/tick # ----------------------------------------------------------------------------- diff --git a/tests/host/log/README.md b/tests/host/log/README.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/host/log/test_log.c b/tests/host/log/test_log.c new file mode 100644 index 0000000..e660803 --- /dev/null +++ b/tests/host/log/test_log.c @@ -0,0 +1,396 @@ +/** + * @file test_log.c + * @brief Unit-тесты для utils/log (Unity + fff). + * + * Категория: A с fff-хуками. + * log.c не зависит от fsl_*.h, но имеет weak-хуки (mutex, timestamp) + * которые мокаются через fff как strong-определения — линкер выбирает + * их поверх weak-реализаций из log.c автоматически. + * + * Захват вывода: + * Вместо реального UART используется статическая функция capture_cb(), + * которая сохраняет содержимое и длину каждого вызова в s_capture. + * Тесты форматирования проверяют s_capture.buf через strstr(). + * + * Группы тестов: + * + * 1. Init — поведение до и после log_init(). + * Проверяет что callback не вызывается при NULL-транспорте, + * и что повторный log_init() заменяет предыдущий callback. + * + * 2. Output — формат строки. + * Проверяет наличие символа уровня ([E]/[W]/[I]/[D]/[V]), + * тега, текста сообщения с форматированием и \r\n в конце. + * + * 3. Timestamp — вызов log_get_timestamp_ms(). + * Проверяет что хук вызывается ровно один раз на каждый LOG_* + * и что возвращённое значение попадает в строку вывода. + * + * 4. Overflow — защита от выхода за границу буфера. + * Подаёт строку длиннее LOG_BUF_SIZE (256 байт). Проверяет что + * callback получил не более 256 байт и строка заканчивается \r\n. + * Именно эта группа поймала баг size_t underflow при первом прогоне + * через ASan (vsnprintf возвращает желаемую длину, а не фактическую). + * + * 5. Mutex — порядок lock → callback → unlock. + * custom_fake для lock/unlock записывает монотонный счётчик s_seq, + * capture_cb делает то же самое. После вызова LOG_* сравниваем числа: + * s_lock_seq < s_cb_seq < s_unlock_seq. + * + * 6. Context — передача p_ctx в callback. + * Проверяет что указатель контекста из log_init() доходит до + * callback без изменений (NULL и ненулевой указатель). + */ + +#include "fff.h" +#include "unity.h" + +DEFINE_FFF_GLOBALS; + +/* ── fff-фейки для weak-хуков log.c ───────────────────────────────────── + * Объявляем ДО включения log/log.h, чтобы линкер видел strong-определения + * раньше weak-реализаций из log.c. */ +FAKE_VOID_FUNC(log_mutex_init); +FAKE_VOID_FUNC(log_mutex_lock); +FAKE_VOID_FUNC(log_mutex_unlock); +FAKE_VALUE_FUNC(uint32_t, log_get_timestamp_ms); + +#include "log/log.h" + +#include +#include + +/* ── Захват вывода ───────────────────────────────────────────────────────── */ + +#define CAPTURE_BUF_SIZE 512U + +static struct +{ + char buf[CAPTURE_BUF_SIZE]; + size_t len; + int call_count; + void *p_ctx; +} g_s_capture; + +static void capture_cb(const char *p_buf, size_t len, void *p_ctx) +{ + g_s_capture.call_count++; + g_s_capture.p_ctx = p_ctx; + + size_t copy = (len < CAPTURE_BUF_SIZE - 1U) ? len : (CAPTURE_BUF_SIZE - 1U); + memcpy(g_s_capture.buf, p_buf, copy); + g_s_capture.buf[copy] = '\0'; + g_s_capture.len = copy; +} + +/* ── setUp / tearDown ────────────────────────────────────────────────────── */ + +void setUp(void) +{ + memset(&g_s_capture, 0, sizeof(g_s_capture)); + + RESET_FAKE(log_mutex_init); + RESET_FAKE(log_mutex_lock); + RESET_FAKE(log_mutex_unlock); + RESET_FAKE(log_get_timestamp_ms); + FFF_RESET_HISTORY(); + + /* Каждый тест начинает с чистого состояния логгера */ + log_init(capture_cb, NULL); +} + +void tearDown(void) +{ +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * 1. Init — поведение до и после log_init() + * ═══════════════════════════════════════════════════════════════════════════ */ + +void test_no_output_before_init(void) +{ + /* Сбросить callback — имитировать состояние до первого log_init() */ + log_init(NULL, NULL); + memset(&g_s_capture, 0, sizeof(g_s_capture)); + + LOG_I("TAG", "message"); + + TEST_ASSERT_EQUAL_INT(0, g_s_capture.call_count); +} + +void test_no_output_with_null_cb(void) +{ + log_init(NULL, NULL); + memset(&g_s_capture, 0, sizeof(g_s_capture)); + + LOG_W("TAG", "message"); + + TEST_ASSERT_EQUAL_INT(0, g_s_capture.call_count); +} + +void test_callback_called_once_per_log(void) +{ + LOG_I("TAG", "first"); + TEST_ASSERT_EQUAL_INT(1, g_s_capture.call_count); + + LOG_I("TAG", "second"); + TEST_ASSERT_EQUAL_INT(2, g_s_capture.call_count); +} + +void test_reinit_replaces_callback(void) +{ + /* После повторного log_init() с NULL — вывода нет */ + log_init(NULL, NULL); + + int count_before = g_s_capture.call_count; + LOG_I("TAG", "should not appear"); + + TEST_ASSERT_EQUAL_INT(count_before, g_s_capture.call_count); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * 2. Output — формат строки: уровень, тег, перевод строки + * ═══════════════════════════════════════════════════════════════════════════ */ + +void test_output_contains_level_error(void) +{ + LOG_E("TAG", "msg"); + TEST_ASSERT_NOT_NULL(strstr(g_s_capture.buf, "[E]")); +} + +void test_output_contains_level_warn(void) +{ + LOG_W("TAG", "msg"); + TEST_ASSERT_NOT_NULL(strstr(g_s_capture.buf, "[W]")); +} + +void test_output_contains_level_info(void) +{ + LOG_I("TAG", "msg"); + TEST_ASSERT_NOT_NULL(strstr(g_s_capture.buf, "[I]")); +} + +void test_output_contains_level_debug(void) +{ + LOG_D("TAG", "msg"); + TEST_ASSERT_NOT_NULL(strstr(g_s_capture.buf, "[D]")); +} + +void test_output_contains_level_verbose(void) +{ + LOG_V("TAG", "msg"); + TEST_ASSERT_NOT_NULL(strstr(g_s_capture.buf, "[V]")); +} + +void test_output_contains_tag(void) +{ + LOG_I("BOOT", "msg"); + TEST_ASSERT_NOT_NULL(strstr(g_s_capture.buf, "[BOOT]")); +} + +void test_output_contains_message(void) +{ + LOG_I("TAG", "hello world"); + TEST_ASSERT_NOT_NULL(strstr(g_s_capture.buf, "hello world")); +} + +void test_output_ends_with_crlf(void) +{ + LOG_I("TAG", "msg"); + + TEST_ASSERT_GREATER_THAN(2U, g_s_capture.len); + TEST_ASSERT_EQUAL_CHAR('\r', g_s_capture.buf[g_s_capture.len - 2U]); + TEST_ASSERT_EQUAL_CHAR('\n', g_s_capture.buf[g_s_capture.len - 1U]); +} + +void test_output_format_message_with_args(void) +{ + LOG_I("TAG", "val=%d", 42); + TEST_ASSERT_NOT_NULL(strstr(g_s_capture.buf, "val=42")); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * 3. Timestamp — вызов log_get_timestamp_ms() и значение в выводе + * ═══════════════════════════════════════════════════════════════════════════ */ + +void test_timestamp_hook_called(void) +{ + LOG_I("TAG", "msg"); + TEST_ASSERT_EQUAL_INT(1, log_get_timestamp_ms_fake.call_count); +} + +void test_timestamp_hook_called_each_log(void) +{ + LOG_I("TAG", "first"); + LOG_I("TAG", "second"); + TEST_ASSERT_EQUAL_INT(2, log_get_timestamp_ms_fake.call_count); +} + +void test_timestamp_value_appears_in_output(void) +{ + log_get_timestamp_ms_fake.return_val = 12345U; + + LOG_I("TAG", "msg"); + + TEST_ASSERT_NOT_NULL(strstr(g_s_capture.buf, "12345")); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * 4. Overflow — буфер переполнен, строка обрезана, \r\n сохранён + * ═══════════════════════════════════════════════════════════════════════════ */ + +void test_overflow_output_not_exceeds_buf_size(void) +{ + /* Строка длиннее LOG_BUF_SIZE (256) */ + char long_msg[300]; + memset(long_msg, 'X', sizeof(long_msg) - 1U); + long_msg[sizeof(long_msg) - 1U] = '\0'; + + LOG_I("TAG", "%s", long_msg); + + /* callback получил не больше LOG_BUF_SIZE байт */ + TEST_ASSERT_LESS_OR_EQUAL(256U, g_s_capture.len); +} + +void test_overflow_output_ends_with_crlf(void) +{ + /* При переполнении \r\n всё равно должен быть в конце */ + char long_msg[300]; + memset(long_msg, 'Y', sizeof(long_msg) - 1U); + long_msg[sizeof(long_msg) - 1U] = '\0'; + + LOG_I("TAG", "%s", long_msg); + + TEST_ASSERT_GREATER_THAN(2U, g_s_capture.len); + TEST_ASSERT_EQUAL_CHAR('\r', g_s_capture.buf[g_s_capture.len - 2U]); + TEST_ASSERT_EQUAL_CHAR('\n', g_s_capture.buf[g_s_capture.len - 1U]); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * 5. Mutex — порядок lock → callback → unlock + * + * Используем глобальную историю вызовов fff (fff.call_history[]). + * lock должен быть вызван раньше callback (но callback — не fff-фейк, + * поэтому порядок проверяем через счётчик последовательности. + * ═══════════════════════════════════════════════════════════════════════════ */ + +static int s_seq = 0; +static int s_lock_seq = 0; +static int s_cb_seq = 0; +static int s_unlock_seq = 0; + +static void seq_lock(void) +{ + s_lock_seq = ++s_seq; +} +static void seq_unlock(void) +{ + s_unlock_seq = ++s_seq; +} + +static void seq_cb(const char *p_buf, size_t len, void *p_ctx) +{ + (void) p_buf; + (void) len; + (void) p_ctx; + s_cb_seq = ++s_seq; +} + +void test_mutex_lock_before_callback(void) +{ + s_seq = s_lock_seq = s_cb_seq = s_unlock_seq = 0; + log_mutex_lock_fake.custom_fake = seq_lock; + log_mutex_unlock_fake.custom_fake = seq_unlock; + log_init(seq_cb, NULL); + + LOG_I("TAG", "msg"); + + TEST_ASSERT_GREATER_THAN(0, s_lock_seq); + TEST_ASSERT_GREATER_THAN(0, s_cb_seq); + TEST_ASSERT_LESS_THAN(s_cb_seq, s_lock_seq); +} + +void test_mutex_unlock_after_callback(void) +{ + s_seq = s_lock_seq = s_cb_seq = s_unlock_seq = 0; + log_mutex_lock_fake.custom_fake = seq_lock; + log_mutex_unlock_fake.custom_fake = seq_unlock; + log_init(seq_cb, NULL); + + LOG_I("TAG", "msg"); + + TEST_ASSERT_GREATER_THAN(s_cb_seq, s_unlock_seq); +} + +void test_mutex_lock_unlock_called_once_per_log(void) +{ + LOG_I("TAG", "msg"); + + TEST_ASSERT_EQUAL_INT(1, log_mutex_lock_fake.call_count); + TEST_ASSERT_EQUAL_INT(1, log_mutex_unlock_fake.call_count); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + * 6. Context — p_ctx передаётся в callback без изменений + * ═══════════════════════════════════════════════════════════════════════════ */ + +void test_ctx_null_passed_to_callback(void) +{ + log_init(capture_cb, NULL); + LOG_I("TAG", "msg"); + TEST_ASSERT_NULL(g_s_capture.p_ctx); +} + +void test_ctx_pointer_passed_to_callback(void) +{ + int dummy = 42; + log_init(capture_cb, &dummy); + + LOG_I("TAG", "msg"); + + TEST_ASSERT_EQUAL_PTR(&dummy, g_s_capture.p_ctx); +} + +/* ── Runner ──────────────────────────────────────────────────────────────── */ + +int main(void) +{ + UNITY_BEGIN(); + + /* Init */ + RUN_TEST(test_no_output_before_init); + RUN_TEST(test_no_output_with_null_cb); + RUN_TEST(test_callback_called_once_per_log); + RUN_TEST(test_reinit_replaces_callback); + + /* Output */ + RUN_TEST(test_output_contains_level_error); + RUN_TEST(test_output_contains_level_warn); + RUN_TEST(test_output_contains_level_info); + RUN_TEST(test_output_contains_level_debug); + RUN_TEST(test_output_contains_level_verbose); + RUN_TEST(test_output_contains_tag); + RUN_TEST(test_output_contains_message); + RUN_TEST(test_output_ends_with_crlf); + RUN_TEST(test_output_format_message_with_args); + + /* Timestamp */ + RUN_TEST(test_timestamp_hook_called); + RUN_TEST(test_timestamp_hook_called_each_log); + RUN_TEST(test_timestamp_value_appears_in_output); + + /* Overflow */ + RUN_TEST(test_overflow_output_not_exceeds_buf_size); + RUN_TEST(test_overflow_output_ends_with_crlf); + + /* Mutex */ + RUN_TEST(test_mutex_lock_before_callback); + RUN_TEST(test_mutex_unlock_after_callback); + RUN_TEST(test_mutex_lock_unlock_called_once_per_log); + + /* Context */ + RUN_TEST(test_ctx_null_passed_to_callback); + RUN_TEST(test_ctx_pointer_passed_to_callback); + + return UNITY_END(); +} \ No newline at end of file diff --git a/utils/CMakeLists.txt b/utils/CMakeLists.txt index 0293854..16dd8a2 100644 --- a/utils/CMakeLists.txt +++ b/utils/CMakeLists.txt @@ -7,8 +7,14 @@ # Не зависит от конкретного железа (нет fsl_*, CMSIS, FreeRTOS). 2. Используется # более чем в одном месте проекта. -add_library(utils STATIC ring_buffer/ring_buffer.c) +add_library(utils STATIC ring_buffer/ring_buffer.c log/log.c) # PUBLIC: любой таргет, слинкованный с utils, автоматически получает корень -# utils/ в include path → #include "ring_buffer/ring_buffer.h" +# utils/ в include path → #include "ring_buffer/ring_buffer.h" #include "log.h" target_include_directories(utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +# LOG_LEVEL пробрасывается из пресета или командной строки. Если не задан — +# log.h выберет сам через NDEBUG. +if(DEFINED LOG_LEVEL) + target_compile_definitions(utils PUBLIC LOG_LEVEL=${LOG_LEVEL}) +endif() diff --git a/utils/README.md b/utils/README.md index 56ad94d..52dd42c 100644 --- a/utils/README.md +++ b/utils/README.md @@ -11,38 +11,5 @@ --- -## Модули -### `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/log/log.c b/utils/log/log.c new file mode 100644 index 0000000..77f17fd --- /dev/null +++ b/utils/log/log.c @@ -0,0 +1,169 @@ +/** + * @file log.c + * @brief Реализация платформонезависимого логгера. + * + * Единственная внешняя зависимость — стандартная библиотека C + * (vsnprintf, stdarg). Транспорт абстрагирован через callback. + * + * Компилируется всегда. При LOG_LEVEL=0 макросы в log.h + * разворачиваются в ((void)0) и log_write() не вызывается вообще — + * но сам .c файл в ROM не попадает: линкер выкидывает неиспользуемые + * секции (--gc-sections). + */ + +#include "log.h" + +#include +#include +#include + +/* -------------------------------------------------------------------------- */ +/* Конфигурация */ +/* -------------------------------------------------------------------------- */ + +/** Размер статического буфера одной строки (включая префикс, \r\n, \0). */ +#ifndef LOG_BUF_SIZE +#define LOG_BUF_SIZE 256U +#endif + +/** Перевод строки. \r\n для MCU-Link VCOM. */ +#ifndef LOG_NEWLINE +#define LOG_NEWLINE "\r\n" +#endif + +/* -------------------------------------------------------------------------- */ +/* Внутреннее состояние */ +/* -------------------------------------------------------------------------- */ + +static log_write_cb_t g_s_write_cb = NULL; +static void *g_s_p_ctx = NULL; + +/* -------------------------------------------------------------------------- */ +/* Weak-хуки мьютекса — NOP для bare-metal */ +/* -------------------------------------------------------------------------- */ + +__attribute__((weak)) void log_mutex_init(void) +{ +} +__attribute__((weak)) void log_mutex_lock(void) +{ +} +__attribute__((weak)) void log_mutex_unlock(void) +{ +} +__attribute__((weak)) uint32_t log_get_timestamp_ms(void) +{ + return 0U; +} + +/* -------------------------------------------------------------------------- */ +/* Вспомогательное: метка уровня */ +/* -------------------------------------------------------------------------- */ + +static char level_char(int level) +{ + switch (level) + { + case LOG_LEVEL_ERROR: + return 'E'; + case LOG_LEVEL_WARN: + return 'W'; + case LOG_LEVEL_INFO: + return 'I'; + case LOG_LEVEL_DEBUG: + return 'D'; + case LOG_LEVEL_VERBOSE: + return 'V'; + default: + return '?'; + } +} + +/* -------------------------------------------------------------------------- */ +/* Публичный API */ +/* -------------------------------------------------------------------------- */ + +void log_init(log_write_cb_t p_write_cb, void *p_ctx) +{ + g_s_write_cb = p_write_cb; + g_s_p_ctx = p_ctx; +} + +void log_write(int level, const char *p_tag, const char *p_fmt, // NOLINT(readability-function-size) + ...) +{ + if (g_s_write_cb == NULL) + { + return; + } + + /* Статический буфер: под мьютексом → нет гонки в FreeRTOS, + * в bare-metal гонки нет по определению. */ + static char s_buf[LOG_BUF_SIZE]; + + log_mutex_lock(); + + /* Префикс: [timestamp][L][TAG] */ + int prefix_len = snprintf(s_buf, sizeof(s_buf), "[%10lu][%c][%s] ", + (unsigned long) log_get_timestamp_ms(), level_char(level), p_tag); + + if (prefix_len < 0) + { + log_mutex_unlock(); + return; + } + + /* Сообщение пользователя */ + size_t prefix_sz = (size_t) prefix_len; + size_t remaining = (prefix_sz < sizeof(s_buf)) ? (sizeof(s_buf) - prefix_sz) : 0U; + + if (remaining > 0U) + { + va_list args; + va_start(args, p_fmt); + int msg_len = vsnprintf(s_buf + prefix_sz, remaining, p_fmt, args); + va_end(args); + + if (msg_len > 0) + { + prefix_sz += (size_t) msg_len; + } + } + + /* ВАЖНО: vsnprintf возвращает число символов которые *хотел* записать, + * а не сколько реально влезло. Если сообщение длиннее remaining, + * prefix_sz > sizeof(s_buf). Без clamp ниже вычисление avail даёт + * size_t underflow → огромное число → выход за границу буфера (UB). */ + if (prefix_sz > sizeof(s_buf)) + { + prefix_sz = sizeof(s_buf); + } + + /* Перевод строки — дописать если влезает */ + const char *p_nl = LOG_NEWLINE; + size_t nl_len = sizeof(LOG_NEWLINE) - 1U; /* без \0 */ + size_t avail = sizeof(s_buf) - prefix_sz; + + if (avail > nl_len) + { + for (size_t i = 0U; i < nl_len; i++) + { + s_buf[prefix_sz + i] = p_nl[i]; + } + prefix_sz += nl_len; + } + else + { + /* Буфер переполнен — принудительно завершить строку */ + size_t tail = sizeof(s_buf) - nl_len; + for (size_t i = 0U; i < nl_len; i++) + { + s_buf[tail + i] = p_nl[i]; + } + prefix_sz = sizeof(s_buf); + } + + g_s_write_cb(s_buf, prefix_sz, g_s_p_ctx); + + log_mutex_unlock(); +} \ No newline at end of file diff --git a/utils/log/log.h b/utils/log/log.h new file mode 100644 index 0000000..5862ff2 --- /dev/null +++ b/utils/log/log.h @@ -0,0 +1,179 @@ +/** + * @file log.h + * @brief Платформонезависимый логгер с callback-транспортом. + * + * Ядро логгера не знает о конкретном транспорте — UART, Flash, USB CDC + * и т.д. Транспорт подключается через log_init() в виде callback-функции. + * Адаптеры под конкретные транспорты живут в port/log_/. + * + * Использование: + * @code + * // main.c — зарегистрировать транспорт (см. port/log/) + * log_init(uart_log_write, NULL); + * + * // любой .c файл + * #include "log.h" + * LOG_I("BOOT", "Started, tick=%lu", (unsigned long)bsp_tick_get_ms()); + * LOG_W("SDIO", "Card not detected"); + * LOG_D("UART", "RX=%u bytes", bsp_uart_host_rx_available()); + * @endcode + * + * Формат вывода: + * [ 1234][I][BOOT] Started, tick=1234\r\n + * + * Уровни (LOG_LEVEL задаётся через CMake -DLOG_LEVEL=N): + * 0 — off все LOG_* → ((void)0), нулевой ROM + * 1 — error + * 2 — warn + * 3 — info + * 4 — debug + * 5 — verbose + * + * @note Вызов LOG_* из ISR запрещён (TX-блокирующий, мьютекс). + */ + +#ifndef LOG_H +#define LOG_H + +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + + /* -------------------------------------------------------------------------- */ + /* Уровни */ + /* -------------------------------------------------------------------------- */ + +#define LOG_LEVEL_OFF 0 +#define LOG_LEVEL_ERROR 1 +#define LOG_LEVEL_WARN 2 +#define LOG_LEVEL_INFO 3 +#define LOG_LEVEL_DEBUG 4 +#define LOG_LEVEL_VERBOSE 5 + +/* Если LOG_LEVEL не задан через CMake — максимальный в Debug, off в Release */ +#ifndef LOG_LEVEL +#ifdef NDEBUG +#define LOG_LEVEL LOG_LEVEL_OFF +#else +#define LOG_LEVEL LOG_LEVEL_VERBOSE +#endif +#endif + + /* -------------------------------------------------------------------------- */ + /* Callback-тип транспорта */ + /* -------------------------------------------------------------------------- */ + + /** + * @brief Тип callback-функции транспорта. + * + * Вызывается логгером для каждой готовой строки лога. + * Реализуется в адаптере транспорта (port/log_uart/, port/log_flash/ и т.д.). + * + * @param p_buf Указатель на строку лога (не нуль-терминирована). + * @param len Длина строки в байтах. + * @param p_ctx Пользовательский контекст, переданный в log_init(). + */ + typedef void (*log_write_cb_t)(const char *p_buf, size_t len, void *p_ctx); + + /* -------------------------------------------------------------------------- */ + /* Инициализация */ + /* -------------------------------------------------------------------------- */ + + /** + * @brief Инициализировать логгер и зарегистрировать транспорт. + * + * Вызывать один раз из main() после инициализации транспорта + * (например, bsp_uart_host_init()) и мьютекса (log_mutex_init()). + * + * @param p_write_cb Callback транспортного адаптера. NULL — логгер молчит. + * @param p_ctx Контекст, передаваемый в write_cb при каждом вызове. + * Для UART обычно NULL. + */ + void log_init(log_write_cb_t p_write_cb, void *p_ctx); + + /* -------------------------------------------------------------------------- */ + /* Мьютекс (weak-хуки) */ + /* -------------------------------------------------------------------------- */ + + /** + * @brief Вернуть текущее время в миллисекундах. + * + * Weak-хук: реализация по умолчанию возвращает 0. + * Переопределяется в адаптере транспорта или в BSP-инициализации: + * + * @code + * // port/log_uart/log_uart.c или firmware/xxx/src/log_time.c + * uint32_t log_get_timestamp_ms(void) { return bsp_tick_get_ms(); } + * @endcode + * + * @return Текущее время в мс. + */ + uint32_t log_get_timestamp_ms(void); + + /** + * @brief Инициализировать мьютекс логгера. + * + * Bare-metal: weak NOP — не вызывать обязательно. + * FreeRTOS: strong-переопределение в firmware/tft_app/src/log_mutex.c. + * Вызывать ДО log_init(). + */ + void log_mutex_init(void); + + /** @brief Захватить мьютекс. Weak NOP для bare-metal. */ + void log_mutex_lock(void); + + /** @brief Освободить мьютекс. Weak NOP для bare-metal. */ + void log_mutex_unlock(void); + + /* -------------------------------------------------------------------------- */ + /* Внутренняя функция — не вызывать напрямую */ + /* -------------------------------------------------------------------------- */ + + /** @cond INTERNAL */ + void log_write(int level, const char *p_tag, const char *p_fmt, ...) + __attribute__((format(printf, 3, 4))); + /** @endcond */ + + /* -------------------------------------------------------------------------- */ + /* Публичные макросы */ + /* -------------------------------------------------------------------------- */ + +#if LOG_LEVEL >= LOG_LEVEL_ERROR +#define LOG_E(tag, fmt, ...) log_write(LOG_LEVEL_ERROR, (tag), (fmt), ##__VA_ARGS__) +#else +#define LOG_E(tag, fmt, ...) ((void) 0) +#endif + +#if LOG_LEVEL >= LOG_LEVEL_WARN +#define LOG_W(tag, fmt, ...) log_write(LOG_LEVEL_WARN, (tag), (fmt), ##__VA_ARGS__) +#else +#define LOG_W(tag, fmt, ...) ((void) 0) +#endif + +#if LOG_LEVEL >= LOG_LEVEL_INFO +#define LOG_I(tag, fmt, ...) log_write(LOG_LEVEL_INFO, (tag), (fmt), ##__VA_ARGS__) +#else +#define LOG_I(tag, fmt, ...) ((void) 0) +#endif + +#if LOG_LEVEL >= LOG_LEVEL_DEBUG +#define LOG_D(tag, fmt, ...) log_write(LOG_LEVEL_DEBUG, (tag), (fmt), ##__VA_ARGS__) +#else +#define LOG_D(tag, fmt, ...) ((void) 0) +#endif + +#if LOG_LEVEL >= LOG_LEVEL_VERBOSE +#define LOG_V(tag, fmt, ...) log_write(LOG_LEVEL_VERBOSE, (tag), (fmt), ##__VA_ARGS__) +#else +#define LOG_V(tag, fmt, ...) ((void) 0) +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* LOG_H */ \ No newline at end of file diff --git a/utils/ring_buffer/README.md b/utils/ring_buffer/README.md new file mode 100644 index 0000000..986f5e5 --- /dev/null +++ b/utils/ring_buffer/README.md @@ -0,0 +1,35 @@ +# util/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-симуляцию).