- добавлен компонент util/log - логгер
- написан адаптер UART для логгера: port/log
- util/log покрыт юнит-тестами
This commit is contained in:
Dmitry Akimov 2026-03-23 12:19:39 +03:00
parent 4570b27e71
commit c4feaa8f45
20 changed files with 1081 additions and 200 deletions

View file

@ -32,8 +32,10 @@ endif()
# SDK и BSP — всегда подключаются, но содержат защиту от host сборки внутри # SDK и BSP — всегда подключаются, но содержат защиту от host сборки внутри
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
add_subdirectory(sdk) add_subdirectory(sdk)
add_subdirectory(bsp)
add_subdirectory(utils) add_subdirectory(utils)
add_subdirectory(bsp)
add_subdirectory(port)
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Сторонние и внутренние библиотеки # Сторонние и внутренние библиотеки
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

View file

@ -140,6 +140,7 @@
"configurePreset": "host-debug", "configurePreset": "host-debug",
"targets": [ "targets": [
"test_bsp_led", "test_bsp_led",
"test_log",
"test_ring_buffer", "test_ring_buffer",
"test_timeout_pattern", "test_timeout_pattern",
"uart_host_mock_example" "uart_host_mock_example"
@ -151,6 +152,7 @@
"configurePreset": "host-release", "configurePreset": "host-release",
"targets": [ "targets": [
"test_bsp_led", "test_bsp_led",
"test_log",
"test_ring_buffer", "test_ring_buffer",
"test_timeout_pattern", "test_timeout_pattern",
"uart_host_mock_example" "uart_host_mock_example"

152
TODO.md
View file

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

View file

@ -15,7 +15,12 @@ target_compile_definitions(${TARGET_NAME}
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
target_link_libraries( target_link_libraries(
${TARGET_NAME} ${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 lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED
) )

View file

@ -3,29 +3,28 @@
#include "bsp/led.h" #include "bsp/led.h"
#include "bsp/tick.h" #include "bsp/tick.h"
#include "bsp/uart_host.h" #include "bsp/uart_host.h"
#include "log/log.h"
#include "port/log_uart.h"
#include <stdint.h>
int main(void) int main(void)
{ {
const uint16_t DELAY_MS = 100; const uint16_t DELAY_MS = 1000;
const uint32_t UART_BAUDRATE = 115200; const uint32_t UART_BAUDRATE = 115200;
board_hw_init(); board_hw_init();
bsp_led_init(); bsp_led_init();
bsp_tick_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; uint32_t cycle = 0;
while (1) while (1)
{ {
bsp_led_on(LED_HEARTBEAT); 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_delay(DELAY_MS);
bsp_led_off(LED_HEARTBEAT); bsp_led_off(LED_HEARTBEAT);
bsp_delay(DELAY_MS); bsp_delay(DELAY_MS);

4
port/CMakeLists.txt Normal file
View file

@ -0,0 +1,4 @@
if(BUILD_TESTS_HOST)
return()
endif()
add_subdirectory(log)

58
port/README.md Normal file
View file

@ -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`.

24
port/log/CMakeLists.txt Normal file
View file

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

51
port/log/README.md Normal file
View file

@ -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(<target> PRIVATE port_log_uart)
```
Транзитивно подтягивает `utils` (содержит `log.h`) и `LOG_LEVEL`.

View file

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

40
port/log/src/log_mutex.c Normal file
View file

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

42
port/log/src/log_uart.c Normal file
View file

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

View file

@ -71,6 +71,20 @@ add_host_test(
${PROJECT_SOURCE_DIR}/utils # для #include "ring_buffer/ring_buffer.h" ${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 # Тест таймаут-паттерна, используемого в bsp/tick
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------

0
tests/host/log/README.md Normal file
View file

396
tests/host/log/test_log.c Normal file
View file

@ -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 <stdint.h>
#include <string.h>
/* ── Захват вывода ───────────────────────────────────────────────────────── */
#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();
}

View file

@ -7,8 +7,14 @@
# Не зависит от конкретного железа (нет fsl_*, CMSIS, FreeRTOS). 2. Используется # Не зависит от конкретного железа (нет 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, автоматически получает корень # 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}) 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()

View file

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

169
utils/log/log.c Normal file
View file

@ -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 <stdarg.h>
#include <stddef.h>
#include <stdio.h>
/* -------------------------------------------------------------------------- */
/* Конфигурация */
/* -------------------------------------------------------------------------- */
/** Размер статического буфера одной строки (включая префикс, \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();
}

179
utils/log/log.h Normal file
View file

@ -0,0 +1,179 @@
/**
* @file log.h
* @brief Платформонезависимый логгер с callback-транспортом.
*
* Ядро логгера не знает о конкретном транспорте UART, Flash, USB CDC
* и т.д. Транспорт подключается через log_init() в виде callback-функции.
* Адаптеры под конкретные транспорты живут в port/log_<transport>/.
*
* Использование:
* @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 <stddef.h>
#include <stdint.h>
#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 */

View file

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