# 10
- добавлен компонент util/log - логгер - написан адаптер UART для логгера: port/log - util/log покрыт юнит-тестами
This commit is contained in:
parent
4570b27e71
commit
c4feaa8f45
20 changed files with 1081 additions and 200 deletions
|
|
@ -32,8 +32,10 @@ endif()
|
|||
# SDK и BSP — всегда подключаются, но содержат защиту от host сборки внутри
|
||||
# -----------------------------------------------------------------------------
|
||||
add_subdirectory(sdk)
|
||||
add_subdirectory(bsp)
|
||||
add_subdirectory(utils)
|
||||
add_subdirectory(bsp)
|
||||
add_subdirectory(port)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Сторонние и внутренние библиотеки
|
||||
# -----------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
152
TODO.md
152
TODO.md
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <stdint.h>
|
||||
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);
|
||||
|
|
|
|||
4
port/CMakeLists.txt
Normal file
4
port/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
if(BUILD_TESTS_HOST)
|
||||
return()
|
||||
endif()
|
||||
add_subdirectory(log)
|
||||
58
port/README.md
Normal file
58
port/README.md
Normal 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
24
port/log/CMakeLists.txt
Normal 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
51
port/log/README.md
Normal 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`.
|
||||
40
port/log/include/port/log_uart.h
Normal file
40
port/log/include/port/log_uart.h
Normal 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
40
port/log/src/log_mutex.c
Normal 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
42
port/log/src/log_uart.c
Normal 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);
|
||||
}
|
||||
|
|
@ -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
|
||||
# -----------------------------------------------------------------------------
|
||||
|
|
|
|||
0
tests/host/log/README.md
Normal file
0
tests/host/log/README.md
Normal file
396
tests/host/log/test_log.c
Normal file
396
tests/host/log/test_log.c
Normal 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();
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
169
utils/log/log.c
Normal 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
179
utils/log/log.h
Normal 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 */
|
||||
35
utils/ring_buffer/README.md
Normal file
35
utils/ring_buffer/README.md
Normal 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-симуляцию).
|
||||
Loading…
Reference in a new issue