-  добавлен модуль bsp/uart_host + хост-тесты с моками
This commit is contained in:
Dmitry Akimov 2026-03-17 13:18:59 +03:00
parent 96be9ce023
commit 26d5df362b
18 changed files with 1183 additions and 11 deletions

View file

@ -130,7 +130,8 @@
"targets": [
"test_bsp_led",
"test_ring_buffer",
"test_timeout_pattern"
"test_timeout_pattern",
"uart_host_mock_example"
]
},
{
@ -140,7 +141,8 @@
"targets": [
"test_bsp_led",
"test_ring_buffer",
"test_timeout_pattern"
"test_timeout_pattern",
"uart_host_mock_example"
]
}
],

View file

@ -162,8 +162,8 @@ just build::build-firmware-test-debug # сборка firmware для тарге
just build::hab-firmware-test-debug # подготовка HAB-образа
# На хосте (вне контейнера):
just flash # прошивка через USB ROM
just flash ... # прошивка через USB ROM
```
> Подробнее о прошивке — [HOW_TO_FLASH.md](HOW_TO_FLASH.md)
> Подробнее о прошивке — [HOW_TO_FLASH.md](docs/HOW_TO_FLASH.md)
> Подробнее об окружении разработки — [docs/DEV_ARCH.md](docs/DEV_ARCH.md)

149
TODO-HIL.md Normal file
View file

@ -0,0 +1,149 @@
# HILархитектура и окружение: итоговые решения
## 1. Общий контекст репозитория
- Монорепозиторий для MIMXRT1052 уже содержит:
- три firmwareпроекта: `firmware/test`, `firmware/bootloader`, `firmware/tft_app`;
- BSP, vendored SDK, Unity + fff, SEGGER RTT;
- devcontainer, CMakePresets, justмодули, двухуровневое тестирование (host + target).
- Цель: добавить HILконтур так, чтобы он органично встроился в существующую архитектуру и CI/CD, без резкого усложнения.
---
## 2. Выбор инструментов для HILтестов
### 2.1. Стек тестирования
Принята связка:
- **Host**: Python + pytest
- оркестрация тестов и стенда;
- фикстуры для управления питанием, прошивкой, реле, CAN/UART;
- интеграция с CI, маркировка тестов, отчёты.
- **Target (DUT)**: Unity
- тесты на C, запускаемые на RT1052;
- вывод результатов по UART (или через RTT при необходимости);
- тестовая прошивка может быть отдельным firmwareпроектом, как уже сделано с `firmware/test`.
- **Промежуточный HILконтроллер**: M5Stamp PLC
- принимает команды от хоста;
- управляет CAN, UART, реле, опто‑входами и прочей обвязкой DUT;
- выполняет только примитивные операции (send frame, toggle relay, read input, reset DUT), вся логика сценариев — в pytest.
### 2.2. Почему так
- Unity уже используется в проекте — нет смысла вводить второй Cфреймворк тестирования.
- Python + pytest хорошо подходит для оркестрации внешних устройств (HIL), даёт удобные фикстуры, параметризацию и отчётность.
- Разделение обязанностей:
- DUT отвечает за внутреннюю логику и инварианты;
- HILконтроллер — за физические воздействия/измерения;
- pytest — за сценарии и проверки.
---
## 3. Выбор платформы и стека для HILконтроллера
### 3.1. Аппаратная платформа
- Выбран **M5Stamp PLC** на базе ESP32S3:
- CANшина, RS485, реле, опто‑входы — всё, что нужно для стенда;
- промышленная обвязка (галваническая развязка на входах, реле на выходах).
### 3.2. Варианты ПО для M5Stamp
Рассматривались:
1. **MicroPython**
- Плюсы: быстрый старт, код на Python, удобно писать CLI.
- Минусы:
- нет готового BSP именно под PLCвариант;
- нужно вручную разбираться с разводкой, CAN/RS485/реле;
- меньше детерминизма по таймингам.
2. **UIFlow**
- Плюсы: визуальное программирование, быстрые демки.
- Минусы:
- ориентирован на учебные/IoTсценарии;
- завязан на WebIDE/облако;
- неудобен для строгого, текстового, версионируемого CLI под pytest/CI.
3. **Arduinoфреймворк** (через Arduino core для ESP32)
- Плюсы:
- официальная поддержка M5плат, библиотек и BSP;
- примеры по работе с реле, RS485, CAN;
- легко поднять простой текстовый CLI по Serial;
- хорошо работает в headlessрежиме через `arduino-cli`.
- Минусы:
- код на C++ (в отличие от MicroPython);
- отдельный маленький проект.
### 3.3. Принятое решение
- Для M5Stamp PLC используется **Arduinoфреймворк** + `arduino-cli`.
- MicroPython и UIFlow откладываются:
- MicroPython — как возможный эксперимент позже;
- UIFlow — не подходит как основа HILконтроллера.
---
## 4. Архитектура HILконтроллера (M5Stamp PLC)
### 4.1. Роль M5Stamp
- Максимально «тупой» исполнитель команд от хоста:
- НЕ содержит сценарной логики тестов;
- предоставляет набор атомарных операций:
- управление реле;
- отправка/приём кадра по CAN/UART;
- переключение питания/Reset DUT;
- чтение опто‑входов/состояния линий.
- Все сложные сценарии, тайминги и проверки живут в pytest на хосте.
### 4.2. Протокол обмена host ↔ M5Stamp
- Простой текстовый CLI по UART/USB:
- команды в виде строк, например:
- `RELAY SET 1 ON`
- `RELAY SET 1 OFF`
- `GPIO READ IN3`
- `CAN SEND 1 123#11223344`
- `UART DUT SEND "AT+PING\r\n"`
- `DUT RESET`
- ответы в понятном формате:
- `OK`
- `ERR <code>`
- `VAL 0|1`
- `EVENT ...` (если понадобится).
- Преимущества:
- легко писать и отлаживать;
- удобно тестировать руками (через serialтерминал);
- протокол можно описать и версионировать как часть репозитория.
---
## 5. Интеграция с существующим репозиторием
### 5.1. Структура репозитория
HILконтроллер хранится в том же монорепо:
```text
/
├── firmware/
│ ├── test/ # тестовая прошивка DUT
│ ├── bootloader/
│ ├── tft_app/
│ └── hil_controller/ # Arduino-проект для M5Stamp PLC (HIL)
├── tools/
│ └── host/
│ ├── hil/ # Python-код, pytest HIL тесты, утилиты
│ ├── flash_usb.py
│ └── ...
├── just/
│ ├── host.just # добавятся host::hil-* цели
│ ├── build.just
│ └── ci.just
└── ...

View file

@ -11,8 +11,10 @@ add_library(
generated/board.c generated/pin_mux.c generated/clock_config.c
generated/startup/startup_MIMXRT1052.S)
add_subdirectory(common)
add_subdirectory(led)
add_subdirectory(tick)
add_subdirectory(uart_host)
# Подавляем предупреждения при компиляции собственных .c файлов библиотеки
target_compile_options(bsp_board PRIVATE -w)
@ -20,18 +22,22 @@ target_compile_options(bsp_board PRIVATE -w)
# SYSTEM подавляет предупреждения для всех внешних потребителей
target_include_directories(bsp_board SYSTEM PUBLIC generated)
# Дефайны PUBLIC — транзитивно подтянутся во все firmware таргеты TODO: для
# каждого приложения свои дефайны
# Дефайны PUBLIC — транзитивно подтянутся во все firmware таргеты
target_compile_definitions(
bsp_board PUBLIC CPU_MIMXRT1052CVJ5B SKIP_SYSCLK_INIT XIP_EXTERNAL_FLASH=1
XIP_BOOT_HEADER_ENABLE=1 XIP_BOOT_HEADER_DCD_ENABLE=1)
# sdk_device PUBLIC — транзитивно подтянется во все firmware таргеты
target_link_libraries(bsp_board PUBLIC sdk_device sdk_clock sdk_common)
target_link_libraries(bsp_board PUBLIC sdk_device sdk_clock sdk_common
sdk_lpuart)
# # Сценарий A: XIP — код исполняется прямо из Flash add_library(bsp_boot_xip
# INTERFACE) target_compile_definitions( bsp_boot_xip INTERFACE
# XIP_EXTERNAL_FLASH=1 XIP_BOOT_HEADER_ENABLE=1 XIP_BOOT_HEADER_DCD_ENABLE=1)
# Выбор boot стратегии (определяется в CMakePresets в зависимости от собираемого
# target)
# TODO: длякаждого приложения свои дефайны # Сценарий A: XIP — код исполняется
# прямо из Flash add_library(bsp_boot_xip INTERFACE) target_compile_definitions(
# bsp_boot_xip INTERFACE XIP_EXTERNAL_FLASH=1 XIP_BOOT_HEADER_ENABLE=1
# XIP_BOOT_HEADER_DCD_ENABLE=1)
# # Сценарий B: копирование в ITCM (для bootloader или критичного кода)
# add_library(bsp_boot_itcm INTERFACE) target_compile_definitions( bsp_boot_itcm

15
bsp/common/CMakeLists.txt Normal file
View file

@ -0,0 +1,15 @@
# =============================================================================
# bsp_status — общий тип возврата для BSP-модулей
#
# INTERFACE-библиотека: нет .c файлов, только пробрасывает include-путь.
# Работает на любой платформе (ARM target и host unit-тесты) без ifdef.
#
# Использование: target_link_libraries(<bsp_module> PUBLIC bsp_status)
# =============================================================================
cmake_minimum_required(VERSION 3.20)
add_library(bsp_status INTERFACE)
target_include_directories(bsp_status
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)

View file

@ -0,0 +1,46 @@
/**
* @file status.h
* @brief Общий тип возврата для BSP-модулей.
*
* Используется модулями, операции которых могут завершиться ошибкой
* в рантайме: uart_host, uart_iso, spi, can, usb, wdog и др.
*
* Не используется там, где ошибка невозможна или означает только
* неправильную конфигурацию: tick, led, gpio (простое управление).
*
* PUBLIC если bsp_status_t есть в публичном заголовке модуля
* PRIVATE если bsp_status_t используется только внутри .c файлов
* target_link_libraries(<target> PUBLIC|PRIVATE bsp_status)
* В коде:
* #include "bsp/status.h"
*/
#ifndef BSP_STATUS_H
#define BSP_STATUS_H
/* -------------------------------------------------------------------------- */
/* Тип */
/* -------------------------------------------------------------------------- */
/**
* @brief Коды возврата BSP-модулей.
*
* Соглашение:
* BSP_OK операция завершена успешно.
* BSP_ERR_* операция завершена с ошибкой; подробности в имени кода.
*
* Значения фиксированы и не меняются: могут сохраняться в NVM / передаваться
* по протоколу.
*/
typedef enum bsp_status_e
{
BSP_OK = 0, /**< Успех. */
BSP_ERR_INIT = 1, /**< Ошибка инициализации / модуль не готов. */
BSP_ERR_TIMEOUT = 2, /**< Истёк таймаут ожидания. */
BSP_ERR_BUSY = 3, /**< Ресурс занят. */
BSP_ERR_PARAM = 4, /**< Невалидный аргумент */
BSP_ERR_OVERFLOW = 5, /**< Переполнение буфера, данные потеряны. */
BSP_ERR_NOT_SUPPORTED = 6, /**< Функция не поддерживается */
} bsp_status_t;
#endif /* BSP_STATUS_H */

View file

@ -0,0 +1,17 @@
if(BUILD_TESTS_HOST)
set(UART_HOST_SOURCES mocks/uart_host_mock.c)
else()
set(UART_HOST_SOURCES src/uart_host.c)
endif()
add_library(bsp_uart_host STATIC ${UART_HOST_SOURCES})
target_include_directories(
bsp_uart_host
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
$<$<BOOL:${BUILD_TESTS_HOST}>:${CMAKE_CURRENT_SOURCE_DIR}/mocks>)
target_link_libraries(
bsp_uart_host
PUBLIC bsp_status
PRIVATE bsp_tick utils sdk_lpuart $<$<BOOL:${BUILD_TESTS_HOST}>:fff>)

164
bsp/uart_host/README.md Normal file
View file

@ -0,0 +1,164 @@
# bsp_uart_host
Коммуникационный канал с хост-машиной через LPUART1 (разъём J2, MCU-Link VCOM).
Применяется для HIL-тестов (pytest + pyserial), отладочного вывода и резервного
канала связи.
---
## Архитектура
```bash
[LPUART1 RX] → LPUART1_IRQHandler → ring_buffer_put()
bsp_uart_host_read() ← polling + таймаут
bsp_uart_host_read_byte()
[LPUART1 TX] ← bsp_uart_host_write() ← LPUART_WriteBlocking()
```
- **TX** — blocking polling (`LPUART_WriteBlocking`). Пакеты короткие, задержка 12 мс приемлема.
- **RX** — ISR пишет в ring buffer, задача/main читает с таймаутом.
- **ISR**`LPUART1_IRQHandler` определён в модуле, модуль владеет прерыванием целиком.
- **Singleton** — один экземпляр, один физический UART.
---
## Быстрый старт
```c
#include "bsp/uart_host.h"
// В main(), после board_hw_init():
bsp_uart_host_init(115200);
// TX
bsp_uart_host_write_str("hello\r\n");
// RX — ждать байт до 100 мс
int32_t byte = bsp_uart_host_read_byte(100);
if (byte < 0) { /* таймаут */ }
// RX — прочитать пакет целиком
uint8_t buf[64];
size_t n = bsp_uart_host_read(buf, sizeof(buf), 500);
```
---
## Конфигурация
Задаётся в CMakeLists.txt **firmware-таргета**, не модуля:
```cmake
target_compile_definitions(firmware_test PRIVATE
BSP_UART_HOST_RX_BUFFER_SIZE=256 # степень двойки, дефолт 256
BSP_UART_HOST_SRC_CLOCK_HZ=24000000
BSP_UART_HOST_IRQ_PRIORITY=5
)
```
| Define | Дефолт | Описание |
|--------|--------|----------|
| `BSP_UART_HOST_RX_BUFFER_SIZE` | `256` | Размер RX ring buffer. **Должен быть степенью двойки.** |
| `BSP_UART_HOST_SRC_CLOCK_HZ` | `24000000` | Частота источника тактирования LPUART1. |
| `BSP_UART_HOST_IRQ_PRIORITY` | `5` | Приоритет `LPUART1_IRQn`. Должен быть ≥ `configMAX_SYSCALL_INTERRUPT_PRIORITY` при использовании FreeRTOS. |
---
## Таймауты
```c
// Без ожидания — вернёт только то, что уже есть в буфере
bsp_uart_host_read(buf, len, 0);
// Ждать с таймаутом (межбайтовый: сбрасывается после каждого принятого байта)
bsp_uart_host_read(buf, len, 100);
// Ждать вечно
bsp_uart_host_read(buf, len, BSP_UART_HOST_WAIT_FOREVER);
```
`bsp_uart_host_read()` возвращает `size_t` — частичное чтение при таймауте
не является ошибкой, caller сам решает что делать с полученным количеством байт.
---
## FreeRTOS
Модуль работает в FreeRTOS без отдельной реализации. При сборке с
`BSP_TICK_FREERTOS_MODE` в цикле ожидания добавляется `vTaskDelay(1)`
задача отдаёт управление планировщику вместо busy-wait.
`BSP_UART_HOST_IRQ_PRIORITY` должен быть установлен ниже
`configMAX_SYSCALL_INTERRUPT_PRIORITY` (числовое значение выше).
---
## Подключение
```cmake
# bsp/CMakeLists.txt
add_subdirectory(common)
add_subdirectory(uart_host)
# firmware/test/CMakeLists.txt
target_link_libraries(firmware_test PRIVATE
bsp_board
bsp_tick
bsp_uart_host
)
```
---
## Тестирование
Для host unit-тестов модуль предоставляет fff-заглушки через **Humble Object**:
в тестовой сборке вместо `uart_host.c` линкуется `mocks/uart_host_mock.c`.
```cmake
# tests/host/CMakeLists.txt
add_host_test(
NAME
uart_host_mock_example
SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/uart_host/test_uart_host.c
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks/uart_host_mock.c
# Если тестируете "protocol.c" который использует uart_host:
# ${CMAKE_SOURCE_DIR}/bsp/protocol/src/protocol.c
INCLUDES
${CMAKE_SOURCE_DIR}/bsp/uart_host/include # bsp/uart_host.h
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks # uart_host_mock.h
${CMAKE_SOURCE_DIR}/bsp/common/include # bsp/status.h
# ${CMAKE_SOURCE_DIR}/bsp/protocol/include # protocol.h
MOCKS
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks/uart_host_mock.c)
```
```c
#include "fff.h"
DEFINE_FFF_GLOBALS;
#include "bsp/uart_host_mock.h"
void setUp(void) { UART_HOST_MOCK_RESET_ALL(); }
void test_something(void) {
bsp_uart_host_write_fake.return_val = BSP_OK;
// ... вызываем тестируемый код ...
TEST_ASSERT_EQUAL(1, bsp_uart_host_write_fake.call_count);
}
```
---
## Зависимости
| Зависимость | Тип | Описание |
|-------------|-----|----------|
| `bsp_status` | PUBLIC | `bsp_status_t` в публичном API |
| `bsp_tick` | PRIVATE | `bsp_tick_get_ms()` для таймаутов |
| `utils` (ring_buffer) | PRIVATE | RX ring buffer |
| `sdk_lpuart` | PRIVATE | `fsl_lpuart.h`, `fsl_clock.h` |

View file

@ -0,0 +1,120 @@
/**
* @file uart_host.h
* @brief BSP: коммуникационный канал с хост-машиной (LPUART1, J2).
*
* Назначение:
* - HIL-тесты (pytest + pyserial через MCU-Link VCOM)
* - Резервный канал связи / отладочный вывод
*
* Архитектура:
* TX blocking polling (LPUART_WriteBlocking)
* RX ISR ring_buffer polling read с таймаутом
*
* Паттерн singleton: один экземпляр на всё приложение.
*
* Размер RX-буфера задаётся через CMake define:
* target_compile_definitions(... PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=256)
* Значение должно быть степенью двойки. Дефолт: 256.
*/
#ifndef BSP_UART_HOST_
#define BSP_UART_HOST_
#include "bsp/status.h"
#include <stddef.h>
#include <stdint.h>
/* -------------------------------------------------------------------------- */
/* Константы */
/* -------------------------------------------------s------------------------- */
/** Передать в timeout_ms чтобы ждать данные бесконечно. */
#define BSP_UART_HOST_WAIT_FOREVER (UINT32_MAX)
/* -------------------------------------------------------------------------- */
/* Инициализация */
/* -------------------------------------------------------------------------- */
/**
* @brief Инициализирует LPUART1: тактирование, пины, прерывание, ring buffer.
*
* Вызывать один раз из main() после board_hw_init().
* Повторный вызов без предварительного deinit вернёт BSP_ERR_INIT.
*
* @param baud_rate Скорость в бод, например 115200.
* @return BSP_OK при успехе, BSP_ERR_INIT при ошибке.
*/
bsp_status_t bsp_uart_host_init(uint32_t baud_rate);
/**
* @brief Деинициализирует LPUART1, отключает прерывание, сбрасывает буфер.
*
* После вызова модуль можно инициализировать повторно.
*/
void bsp_uart_host_deinit(void);
/* -------------------------------------------------------------------------- */
/* TX — blocking polling */
/* -------------------------------------------------------------------------- */
/**
* @brief Отправляет массив байт. Блокирует до завершения передачи.
*
* @param p_data Указатель на буфер данных.
* @param len Количество байт для отправки.
* @return BSP_OK или BSP_ERR_INIT если модуль не инициализирован.
*/
bsp_status_t bsp_uart_host_write(const uint8_t *p_data, size_t len);
/**
* @brief Отправляет C-строку (без нулевого терминатора).
*
* @param p_str Нуль-терминированная строка.
* @return BSP_OK или BSP_ERR_INIT если модуль не инициализирован.
*/
bsp_status_t bsp_uart_host_write_str(const char *p_str);
/* -------------------------------------------------------------------------- */
/* RX — ring buffer + polling с таймаутом */
/* -------------------------------------------------------------------------- */
/**
* @brief Читает до @p len байт с таймаутом.
*
* Функция возвращает управление, как только:
* - прочитано @p len байт, ИЛИ
* - истёк @p timeout_ms с момента последнего поступившего байта,
* ИЛИ буфер пуст и @p timeout_ms == 0.
*
* Частичное чтение не ошибка; caller сам проверяет возвращённое значение.
*
* @param p_buf Буфер для записи принятых данных.
* @param len Максимальное число байт для чтения.
* @param timeout_ms Таймаут ожидания в мс. 0 без ожидания,
* BSP_UART_HOST_WAIT_FOREVER ждать бесконечно.
* @return Число фактически прочитанных байт (0..len).
*/
size_t bsp_uart_host_read(uint8_t *p_buf, size_t len, uint32_t timeout_ms);
/**
* @brief Читает один байт с таймаутом.
*
* @param timeout_ms Таймаут в мс. 0 без ожидания,
* BSP_UART_HOST_WAIT_FOREVER ждать бесконечно.
* @return Принятый байт (0..255) или -1 при таймауте / не инициализирован.
*/
int32_t bsp_uart_host_read_byte(uint32_t timeout_ms);
/**
* @brief Возвращает число байт, доступных в RX-буфере прямо сейчас.
*
* @return Число байт (0 если буфер пуст или модуль не инициализирован).
*/
size_t bsp_uart_host_rx_available(void);
/**
* @brief Сбрасывает содержимое RX-буфера.
*/
void bsp_uart_host_rx_flush(void);
#endif //BSP_UART_HOST_

View file

@ -0,0 +1,41 @@
/**
* @file uart_host_mock.c
* @brief fff-заглушки bsp_uart_host для host unit-тестов (Humble Object).
*
* Подключается вместо uart_host.c при BUILD_TESTS_HOST.
* Не требует SDK, LPUART, прерываний.
*
* Использование в тесте:
*
* #include "fff.h"
* DEFINE_FFF_GLOBALS;
*
* #include "bsp/uart_host.h"
* #include "uart_host_mock.h" // объявления FAKE_*
*
* void setUp(void) { UART_HOST_MOCK_RESET_ALL(); }
* void tearDown(void) {}
*
* void test_something(void) {
* bsp_uart_host_write_fake.return_val = BSP_OK;
* // ... вызываем код под тестом ...
* TEST_ASSERT_EQUAL(1, bsp_uart_host_write_fake.call_count);
* }
*/
#include "uart_host_mock.h"
/* -------------------------------------------------------------------------- */
/* Определения fff-заглушек */
/* -------------------------------------------------------------------------- */
DEFINE_FAKE_VALUE_FUNC(bsp_status_t, bsp_uart_host_init, uint32_t);
DEFINE_FAKE_VOID_FUNC(bsp_uart_host_deinit);
DEFINE_FAKE_VALUE_FUNC(bsp_status_t, bsp_uart_host_write, const uint8_t *, size_t);
DEFINE_FAKE_VALUE_FUNC(bsp_status_t, bsp_uart_host_write_str, const char *);
DEFINE_FAKE_VALUE_FUNC(size_t, bsp_uart_host_read, uint8_t *, size_t, uint32_t);
DEFINE_FAKE_VALUE_FUNC(int32_t, bsp_uart_host_read_byte, uint32_t);
DEFINE_FAKE_VALUE_FUNC(size_t, bsp_uart_host_rx_available);
DEFINE_FAKE_VOID_FUNC(bsp_uart_host_rx_flush);

View file

@ -0,0 +1,55 @@
/**
* @file uart_host_mock.h
* @brief Объявления fff-заглушек bsp_uart_host.
*
* Включать только в test-файлах, не в продакшн-коде.
*
* Порядок include в тест-файле:
* 1. fff.h
* 2. DEFINE_FFF_GLOBALS;
* 3. bsp/uart_host.h публичный API (сигнатуры функций)
* 4. uart_host_mock.h FAKE_* структуры
*/
#ifndef UART_HOST_MOCK_H
#define UART_HOST_MOCK_H
#include "bsp/status.h"
#include "bsp/uart_host.h"
#include "fff.h"
#include <stddef.h>
#include <stdint.h>
/* -------------------------------------------------------------------------- */
/* Объявления заглушек (DEFINE_ живут в uart_host_mock.c) */
/* -------------------------------------------------------------------------- */
DECLARE_FAKE_VALUE_FUNC(bsp_status_t, bsp_uart_host_init, uint32_t);
DECLARE_FAKE_VOID_FUNC(bsp_uart_host_deinit);
DECLARE_FAKE_VALUE_FUNC(bsp_status_t, bsp_uart_host_write, const uint8_t *, size_t);
DECLARE_FAKE_VALUE_FUNC(bsp_status_t, bsp_uart_host_write_str, const char *);
DECLARE_FAKE_VALUE_FUNC(size_t, bsp_uart_host_read, uint8_t *, size_t, uint32_t);
DECLARE_FAKE_VALUE_FUNC(int32_t, bsp_uart_host_read_byte, uint32_t);
DECLARE_FAKE_VALUE_FUNC(size_t, bsp_uart_host_rx_available);
DECLARE_FAKE_VOID_FUNC(bsp_uart_host_rx_flush);
/* -------------------------------------------------------------------------- */
/* Удобный макрос: сброс всех заглушек в setUp() */
/* -------------------------------------------------------------------------- */
/* clang-format off */
#define UART_HOST_MOCK_RESET_ALL() \
RESET_FAKE(bsp_uart_host_init); \
RESET_FAKE(bsp_uart_host_deinit); \
RESET_FAKE(bsp_uart_host_write); \
RESET_FAKE(bsp_uart_host_write_str); \
RESET_FAKE(bsp_uart_host_read); \
RESET_FAKE(bsp_uart_host_read_byte); \
RESET_FAKE(bsp_uart_host_rx_available); \
RESET_FAKE(bsp_uart_host_rx_flush)
/* clang-format on */
#endif /* UART_HOST_MOCK_H */

View file

@ -0,0 +1,266 @@
/**
* @file uart_host.c
* @brief Реализация bsp_uart_host (ARM target only).
*
* Скомпилируется только при сборке под ARM (не при BUILD_TESTS_HOST).
* Для host unit-тестов используется uart_host_mock.c.
*/
#include "bsp/uart_host.h"
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
/* SDK */
#include "fsl_clock.h"
#include "fsl_lpuart.h"
/* BSP */
#include "bsp/tick.h"
/* Utils */
#include "ring_buffer/ring_buffer.h"
#if defined(BSP_TICK_FREERTOS_MODE)
#include "FreeRTOS.h"
#include "task.h"
#endif
/* -------------------------------------------------------------------------- */
/* Конфигурация */
/* -------------------------------------------------------------------------- */
#ifndef BSP_UART_HOST_RX_BUFFER_SIZE
#define BSP_UART_HOST_RX_BUFFER_SIZE (256U)
#endif
/* Частота источника тактирования LPUART1.
* BOARD_BootClockRUN() настраивает OSC 24 MHz на LPUART.
* Скорректируй если у вас другой clock source. */
#ifndef BSP_UART_HOST_SRC_CLOCK_HZ
#define BSP_UART_HOST_SRC_CLOCK_HZ (24000000U)
#endif
/* Приоритет прерывания LPUART1 (0 = наивысший на CM7). */
#ifndef BSP_UART_HOST_IRQ_PRIORITY
#define BSP_UART_HOST_IRQ_PRIORITY (5U)
#endif
/* -------------------------------------------------------------------------- */
/* Статическое состояние модуля */
/* -------------------------------------------------------------------------- */
static uint8_t g_s_rx_buf[BSP_UART_HOST_RX_BUFFER_SIZE];
static ring_buffer_desc_t g_s_rx_ring;
static bool g_s_initialized = false;
/* -------------------------------------------------------------------------- */
/* ISR — владеет прерыванием LPUART1 */
/* -------------------------------------------------------------------------- */
void LPUART1_IRQHandler(void)
{
/* Читаем все байты, которые накопились в RX FIFO. */
while (LPUART_GetStatusFlags(LPUART1) & kLPUART_RxDataRegFullFlag)
{
uint8_t byte = LPUART_ReadByte(LPUART1);
/* Переполнение кольцевого буфера: байт молча теряется.
* Caller должен читать достаточно быстро. */
(void) ring_buffer_put(&g_s_rx_ring, byte);
}
/* Сброс флага прерывания выполняется автоматически при чтении регистра. */
SDK_ISR_EXIT_BARRIER;
}
/* -------------------------------------------------------------------------- */
/* Инициализация / деинициализация */
/* -------------------------------------------------------------------------- */
bsp_status_t bsp_uart_host_init(uint32_t baud_rate)
{
if (g_s_initialized)
{
return BSP_ERR_INIT;
}
/* Инициализация кольцевого буфера. */
if (!ring_buffer_init(&g_s_rx_ring, g_s_rx_buf, BSP_UART_HOST_RX_BUFFER_SIZE))
{
/* Размер не степень двойки — ошибка конфигурации. */
return BSP_ERR_INIT;
}
/* Тактирование LPUART1. */
CLOCK_EnableClock(kCLOCK_Lpuart1);
/* Настройка периферии. */
lpuart_config_t config;
LPUART_GetDefaultConfig(&config);
config.baudRate_Bps = baud_rate;
config.enableRx = true;
config.enableTx = true;
status_t sdk_status = LPUART_Init(LPUART1, &config, BSP_UART_HOST_SRC_CLOCK_HZ);
if (sdk_status != kStatus_Success)
{
CLOCK_DisableClock(kCLOCK_Lpuart1);
return BSP_ERR_INIT;
}
/* Включаем прерывание на приход байта. */
LPUART_EnableInterrupts(LPUART1, kLPUART_RxDataRegFullInterruptEnable);
NVIC_SetPriority(LPUART1_IRQn, BSP_UART_HOST_IRQ_PRIORITY);
EnableIRQ(LPUART1_IRQn);
g_s_initialized = true;
return BSP_OK;
}
void bsp_uart_host_deinit(void)
{
if (!g_s_initialized)
{
return;
}
DisableIRQ(LPUART1_IRQn);
LPUART_DisableInterrupts(LPUART1, kLPUART_RxDataRegFullInterruptEnable);
LPUART_Deinit(LPUART1);
CLOCK_DisableClock(kCLOCK_Lpuart1);
ring_buffer_reset(&g_s_rx_ring);
g_s_initialized = false;
}
/* -------------------------------------------------------------------------- */
/* TX */
/* -------------------------------------------------------------------------- */
bsp_status_t bsp_uart_host_write(const uint8_t *p_data, size_t len)
{
if (!g_s_initialized || p_data == NULL || len == 0U)
{
return BSP_ERR_INIT;
}
LPUART_WriteBlocking(LPUART1, p_data, len);
return BSP_OK;
}
bsp_status_t bsp_uart_host_write_str(const char *p_str)
{
if (!g_s_initialized || p_str == NULL)
{
return BSP_ERR_INIT;
}
size_t len = strlen(p_str);
if (len == 0U)
{
return BSP_OK;
}
LPUART_WriteBlocking(LPUART1, (const uint8_t *) p_str, len);
return BSP_OK;
}
/* -------------------------------------------------------------------------- */
/* RX */
/* -------------------------------------------------------------------------- */
size_t bsp_uart_host_read(uint8_t *p_buf, size_t len, uint32_t timeout_ms)
{
if (!g_s_initialized || p_buf == NULL || len == 0U)
{
return 0U;
}
size_t received = 0U;
uint32_t start = bsp_tick_get_ms();
while (received < len)
{
uint8_t byte;
if (ring_buffer_get(&g_s_rx_ring, &byte))
{
p_buf[received++] = byte;
/* Сбрасываем таймер после каждого принятого байта:
* timeout_ms межбайтовый таймаут, а не общий. */
start = bsp_tick_get_ms();
continue;
}
/* Буфер пуст — проверяем таймаут. */
if (timeout_ms == 0U)
{
break;
}
if (timeout_ms != BSP_UART_HOST_WAIT_FOREVER)
{
if ((bsp_tick_get_ms() - start) >= timeout_ms)
{
break;
}
}
#if defined(BSP_TICK_FREERTOS_MODE)
vTaskDelay(1); // ← отдаём управление планировщику на 1 тик
#endif
}
return received;
}
int32_t bsp_uart_host_read_byte(uint32_t timeout_ms)
{
if (!g_s_initialized)
{
return -1;
}
uint32_t start = bsp_tick_get_ms();
while (true)
{
uint8_t byte;
if (ring_buffer_get(&g_s_rx_ring, &byte))
{
return (int32_t) byte;
}
if (timeout_ms == 0U)
{
break;
}
if (timeout_ms != BSP_UART_HOST_WAIT_FOREVER)
{
if ((bsp_tick_get_ms() - start) >= timeout_ms)
{
break;
}
}
}
return -1;
}
size_t bsp_uart_host_rx_available(void)
{
if (!g_s_initialized)
{
return 0U;
}
return ring_buffer_count(&g_s_rx_ring);
}
void bsp_uart_host_rx_flush(void)
{
if (!g_s_initialized)
{
return;
}
ring_buffer_reset(&g_s_rx_ring);
}

View file

@ -6,6 +6,9 @@ add_executable(firmware_test)
target_sources(firmware_test
PRIVATE main.c ${PROJECT_SOURCE_DIR}/bsp/generated/syscalls.c)
target_compile_definitions(firmware_test
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512)
#
# -----------------------------------------------------------------------------
# Зависимости — только то что нужно для входного контроля bsp_board транзитивно
# даёт: sdk_device, sdk_clock, sdk_common, CPU_MIMXRT1052CVJ5B, XIP_* дефайны

View file

@ -8,7 +8,7 @@ int main(void)
board_hw_init();
bsp_led_init();
bsp_tick_init();
const uint16_t DELAY_MS = 1000;
const uint16_t DELAY_MS = 50;
while (1)
{
bsp_led_on(LED_HEARTBEAT);

View file

@ -78,3 +78,23 @@ add_host_test(
NAME test_timeout_pattern SOURCES timeout/test_timeout_pattern.c
# нет INCLUDES и MOCKS — чистая математика, нет зависимостей
)
# -----------------------------------------------------------------------------
# Тест кода, использующего bsp_uart_host через мок (замените SOURCES на свой
# реальный тест и код под тестом)
# -----------------------------------------------------------------------------
add_host_test(
NAME
uart_host_mock_example
SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/uart_host/test_uart_host.c
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks/uart_host_mock.c
# Если тестируете "protocol.c" который использует uart_host:
# ${CMAKE_SOURCE_DIR}/bsp/protocol/src/protocol.c
INCLUDES
${CMAKE_SOURCE_DIR}/bsp/uart_host/include # bsp/uart_host.h
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks # uart_host_mock.h
${CMAKE_SOURCE_DIR}/bsp/common/include # bsp/status.h
# ${CMAKE_SOURCE_DIR}/bsp/protocol/include # protocol.h
MOCKS
${CMAKE_SOURCE_DIR}/bsp/uart_host/mocks/uart_host_mock.c)

View file

@ -0,0 +1,221 @@
/**
* @file test_uart_host.c
* @brief Unit-тесты для кода использующего bsp_uart_host через fff-мок.
*
* Сборка: host (x86), без SDK, без железа.
* Фреймворки: Unity (assertions) + fff (mocking).
*
* Что тестируем:
* Не саму реализацию uart_host.c (она тестируется HIL-тестами через pyserial),
* а то что код верхнего уровня правильно вызывает API uart_host:
* корректные аргументы, правильная обработка возвращаемых значений,
* поведение при ошибках (BSP_ERR_INIT, таймаут и т.д.).
*/
/* ── 1. fff — до любых mock-заголовков ─────────────────────────────────── */
#include "fff.h"
DEFINE_FFF_GLOBALS;
/* ── 2. Мок uart_host (внутри подтягивает bsp/uart_host.h) ─────────────── */
#include "uart_host_mock.h"
/* ── 3. Unity ───────────────────────────────────────────────────────────── */
#include "unity.h"
/* ── 4. Тестируемый модуль — раскомментировать когда появится ──────────── */
/* #include "protocol/protocol.h" */
/* -------------------------------------------------------------------------- */
void setUp(void)
{
UART_HOST_MOCK_RESET_ALL();
}
void tearDown(void)
{
}
/* -------------------------------------------------------------------------- */
/* init */
/* -------------------------------------------------------------------------- */
void test_init_called_once_with_correct_baud_rate(void)
{
bsp_uart_host_init_fake.return_val = BSP_OK;
bsp_status_t status = bsp_uart_host_init(115200U);
TEST_ASSERT_EQUAL(BSP_OK, status);
TEST_ASSERT_EQUAL(1, bsp_uart_host_init_fake.call_count);
TEST_ASSERT_EQUAL(115200U, bsp_uart_host_init_fake.arg0_val);
}
void test_init_returns_err_on_failure(void)
{
bsp_uart_host_init_fake.return_val = BSP_ERR_INIT;
bsp_status_t status = bsp_uart_host_init(115200U);
TEST_ASSERT_EQUAL(BSP_ERR_INIT, status);
}
/* -------------------------------------------------------------------------- */
/* write */
/* -------------------------------------------------------------------------- */
void test_write_called_with_correct_args(void)
{
const uint8_t payload[] = { 0xAA, 0xBB, 0xCC };
bsp_uart_host_write_fake.return_val = BSP_OK;
bsp_status_t status = bsp_uart_host_write(payload, sizeof(payload));
TEST_ASSERT_EQUAL(BSP_OK, status);
TEST_ASSERT_EQUAL(1, bsp_uart_host_write_fake.call_count);
TEST_ASSERT_EQUAL_PTR(payload, bsp_uart_host_write_fake.arg0_val);
TEST_ASSERT_EQUAL(sizeof(payload), bsp_uart_host_write_fake.arg1_val);
}
void test_write_str_called_with_correct_string(void)
{
bsp_uart_host_write_str_fake.return_val = BSP_OK;
bsp_status_t status = bsp_uart_host_write_str("hello\r\n");
TEST_ASSERT_EQUAL(BSP_OK, status);
TEST_ASSERT_EQUAL_STRING("hello\r\n", bsp_uart_host_write_str_fake.arg0_val);
}
void test_write_returns_err_when_not_initialized(void)
{
bsp_uart_host_write_fake.return_val = BSP_ERR_INIT;
bsp_status_t status = bsp_uart_host_write(NULL, 0U);
TEST_ASSERT_EQUAL(BSP_ERR_INIT, status);
}
/* -------------------------------------------------------------------------- */
/* read_byte */
/* -------------------------------------------------------------------------- */
void test_read_byte_returns_byte_on_success(void)
{
bsp_uart_host_read_byte_fake.return_val = 0x42;
int32_t byte = bsp_uart_host_read_byte(100U);
TEST_ASSERT_EQUAL(0x42, byte);
TEST_ASSERT_EQUAL(100U, bsp_uart_host_read_byte_fake.arg0_val);
}
void test_read_byte_returns_minus1_on_timeout(void)
{
bsp_uart_host_read_byte_fake.return_val = -1;
int32_t byte = bsp_uart_host_read_byte(100U);
TEST_ASSERT_EQUAL(-1, byte);
}
void test_read_byte_wait_forever_passes_correct_timeout(void)
{
bsp_uart_host_read_byte_fake.return_val = 0x01;
(void) bsp_uart_host_read_byte(BSP_UART_HOST_WAIT_FOREVER);
TEST_ASSERT_EQUAL(UINT32_MAX, bsp_uart_host_read_byte_fake.arg0_val);
}
/* -------------------------------------------------------------------------- */
/* read */
/* -------------------------------------------------------------------------- */
void test_read_returns_number_of_bytes_read(void)
{
uint8_t buf[16];
bsp_uart_host_read_fake.return_val = 5U;
size_t n = bsp_uart_host_read(buf, sizeof(buf), 200U);
TEST_ASSERT_EQUAL(5U, n);
TEST_ASSERT_EQUAL_PTR(buf, bsp_uart_host_read_fake.arg0_val);
TEST_ASSERT_EQUAL(sizeof(buf), bsp_uart_host_read_fake.arg1_val);
TEST_ASSERT_EQUAL(200U, bsp_uart_host_read_fake.arg2_val);
}
void test_read_partial_is_not_an_error(void)
{
uint8_t buf[64];
/* Запросили 64, получили 3 — частичное чтение, не ошибка */
bsp_uart_host_read_fake.return_val = 3U;
size_t n = bsp_uart_host_read(buf, sizeof(buf), 500U);
TEST_ASSERT_EQUAL(3U, n);
}
/* -------------------------------------------------------------------------- */
/* rx helpers */
/* -------------------------------------------------------------------------- */
void test_rx_available_returns_count(void)
{
bsp_uart_host_rx_available_fake.return_val = 42U;
size_t n = bsp_uart_host_rx_available();
TEST_ASSERT_EQUAL(42U, n);
}
void test_rx_flush_called_once(void)
{
bsp_uart_host_rx_flush();
TEST_ASSERT_EQUAL(1, bsp_uart_host_rx_flush_fake.call_count);
}
/* -------------------------------------------------------------------------- */
/* setUp сбрасывает счётчики между тестами */
/* -------------------------------------------------------------------------- */
void test_mock_reset_clears_state(void)
{
bsp_uart_host_write_fake.return_val = BSP_OK;
(void) bsp_uart_host_write(NULL, 0U);
TEST_ASSERT_EQUAL(1, bsp_uart_host_write_fake.call_count);
/* setUp() следующего теста вызовет UART_HOST_MOCK_RESET_ALL() */
UART_HOST_MOCK_RESET_ALL();
TEST_ASSERT_EQUAL(0, bsp_uart_host_write_fake.call_count);
TEST_ASSERT_EQUAL(0, bsp_uart_host_write_fake.return_val);
}
/* -------------------------------------------------------------------------- */
int main(void)
{
UNITY_BEGIN();
RUN_TEST(test_init_called_once_with_correct_baud_rate);
RUN_TEST(test_init_returns_err_on_failure);
RUN_TEST(test_write_called_with_correct_args);
RUN_TEST(test_write_str_called_with_correct_string);
RUN_TEST(test_write_returns_err_when_not_initialized);
RUN_TEST(test_read_byte_returns_byte_on_success);
RUN_TEST(test_read_byte_returns_minus1_on_timeout);
RUN_TEST(test_read_byte_wait_forever_passes_correct_timeout);
RUN_TEST(test_read_returns_number_of_bytes_read);
RUN_TEST(test_read_partial_is_not_an_error);
RUN_TEST(test_rx_available_returns_count);
RUN_TEST(test_rx_flush_called_once);
RUN_TEST(test_mock_reset_clears_state);
return UNITY_END();
}

View file

@ -0,0 +1,47 @@
# firmware/test/CMakeLists.txt Тестовая прошивка — входной контроль платы на
# производстве
add_executable(firmware_test)
target_sources(firmware_test
PRIVATE main.c ${PROJECT_SOURCE_DIR}/bsp/generated/syscalls.c)
target_compile_definitions(firmware_test
PRIVATE BSP_UART_HOST_RX_BUFFER_SIZE=512)
# -----------------------------------------------------------------------------
# Зависимости — только то что нужно для входного контроля bsp_board транзитивно
# даёт: sdk_device, sdk_clock, sdk_common, CPU_MIMXRT1052CVJ5B, XIP_* дефайны
# -----------------------------------------------------------------------------
target_link_libraries(
firmware_test
PRIVATE bsp_board bsp_led bsp_tick
# Опциональные библиотеки
lib_external # SEGGER RTT если включён через SEGGER_RTT_ENABLED
)
# -----------------------------------------------------------------------------
# Linker script
# -----------------------------------------------------------------------------
# --gc-sections — удалять неиспользуемые секции (работает с
# -ffunction/data-sections) --print-memory-usage — выводить таблицу
# использования Flash/RAM после линковки -Map — генерировать
# map-файл для анализа размещения символов -T — линкерный
# скрипт с описанием карты памяти IMXRT1052
target_link_options(
firmware_test PRIVATE -Wl,--gc-sections -Wl,--print-memory-usage
-Wl,-Map=${CMAKE_BINARY_DIR}/firmware_test.map
-T${PROJECT_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_flexspi_nor.ld)
# TODO: дефайны BOOT
# -----------------------------------------------------------------------------
# Post-build: генерация .bin для прошивки через blhost
# -----------------------------------------------------------------------------
add_custom_command(
TARGET firmware_test
POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:firmware_test>
${CMAKE_BINARY_DIR}/firmware_test.bin
COMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:firmware_test>
${CMAKE_BINARY_DIR}/firmware_test.hex
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:firmware_test>
COMMENT "Generating firmware_test.bin")