# Created USB-CDC(ACM) stack + HIL tests

This commit is contained in:
Dmitry Akimov 2026-04-06 12:55:11 +03:00
parent dc5cfac386
commit 4b190eba79
10 changed files with 325 additions and 3 deletions

View file

@ -46,3 +46,7 @@ HIL_READY_TIMEOUT=5.0
HIL_M5_TIMEOUT=3.0 HIL_M5_TIMEOUT=3.0
HIL_PYOCD_FREQUENCY=1000000 HIL_PYOCD_FREQUENCY=1000000
HIL_BUILD_DIR=build/target-debug HIL_BUILD_DIR=build/target-debug
# Настройки порта USB CDC ACM на таргете
HIL_USB_CDC_PORT=/dev/cu.usbmodemZZZZ
HIL_USB_CDC_BAUD=115200
HIL_USB_CDC_TIMEOUT=5.0

View file

@ -174,6 +174,7 @@
"test_host_uart", "test_host_uart",
"test_hil_button", "test_hil_button",
"test_hil_can", "test_hil_can",
"test_hil_usb_cdc",
"test_hil_opto" "test_hil_opto"
] ]
} }

View file

@ -9,7 +9,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
#include <string.h>
int main(void) int main(void)
{ {
const uint16_t DELAY_MS = 100; const uint16_t DELAY_MS = 100;
@ -38,6 +38,12 @@ int main(void)
LOG_I("BOOT", "USB CDC ACM ready"); LOG_I("BOOT", "USB CDC ACM ready");
} }
if (is_connection_established == true)
{
const char *msg = "Hello from TFT Board\r\n";
bsp_usb_cdc_write((const uint8_t *) msg, strlen(msg));
}
bsp_usb_cdc_poll(); bsp_usb_cdc_poll();
bsp_led_toggle(LED_HEARTBEAT); bsp_led_toggle(LED_HEARTBEAT);
bsp_delay(DELAY_MS); bsp_delay(DELAY_MS);

View file

@ -334,7 +334,7 @@ _hil_build := BUILD_DIR / "target-debug"
[group('hil')] [group('hil')]
hil-run: hil-run:
HIL_BUILD_DIR={{ _hil_build }} \ HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest -m "not interactive" -v uv run --directory {{ HIL_DIR }} pytest -m "not interactive and not usb_vcom" -v
[doc('Запустить все интерактивные HIL-тесты: кнопки, дисплей')] [doc('Запустить все интерактивные HIL-тесты: кнопки, дисплей')]
[group('hil')] [group('hil')]
@ -366,6 +366,12 @@ hil-can:
HIL_BUILD_DIR={{ _hil_build }} \ HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest 03_test_can.py -v uv run --directory {{ HIL_DIR }} pytest 03_test_can.py -v
[doc('Запустить HIL-тест USB CDC')]
[group('hil')]
hil-usb-cdc:
HIL_BUILD_DIR={{ _hil_build }} \
uv run --directory {{ HIL_DIR }} pytest 05_test_usb_cdc.py -v
# ============================================================================= # =============================================================================
# ГРУППА: debug — GDB-сервер для отладки из VSCode (devcontainer) # ГРУППА: debug — GDB-сервер для отладки из VSCode (devcontainer)
# #

View file

@ -2,3 +2,4 @@ add_subdirectory(host_uart)
add_subdirectory(hil_opto) add_subdirectory(hil_opto)
add_subdirectory(hil_can) add_subdirectory(hil_can)
add_subdirectory(hil_button) add_subdirectory(hil_button)
add_subdirectory(hil_usb_cdc)

View file

@ -0,0 +1,21 @@
set(TARGET_NAME test_hil_usb_cdc)
add_executable(${TARGET_NAME} main.c ${BSP_GENERATED}/clock_config.c
${BSP_STARTUP_FILE} ${BSP_SYSCALLS_FILE})
target_link_options(
${TARGET_NAME}
PRIVATE
-T${CMAKE_SOURCE_DIR}/cmake/linker/MIMXRT1052xxxxx_ram.ld
-Wl,--gc-sections
-Wl,--print-memory-usage
-Wl,-Map=${CMAKE_CURRENT_BINARY_DIR}/${TARGET_NAME}.map)
target_link_libraries(${TARGET_NAME} PRIVATE bsp_boot_ram bsp_board bsp_led
bsp_tick bsp_uart_host bsp_usb_cdc)
add_custom_command(
TARGET ${TARGET_NAME}
POST_BUILD
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${TARGET_NAME}>
COMMENT "Size: ${TARGET_NAME}")

View file

@ -0,0 +1,122 @@
#include "board.h"
#include "bsp/led.h"
#include "bsp/tick.h"
#include "bsp/uart_host.h"
#include "bsp/usb_cdc.h"
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#define CLI_BAUD_RATE 115200U
#define CLI_LINE_MAX 128U
#define CLI_RX_TIMEOUT 50U
/* ---- UART CLI (управляющий канал через MCU-Link VCOM) ---- */
static size_t cli_read_line(uint8_t *p_buf, size_t max_len)
{
size_t pos = 0U;
while (pos < (max_len - 1U))
{
int32_t byte = bsp_uart_host_read_byte(CLI_RX_TIMEOUT);
if (byte < 0)
{
break;
}
if ((char) byte == '\r')
{
continue;
}
if ((char) byte == '\n')
{
break;
}
p_buf[pos++] = (uint8_t) byte;
}
p_buf[pos] = '\0';
return pos;
}
static void cli_process_line(const char *p_line)
{
if (strncmp(p_line, "PING", 4U) == 0)
{
bsp_uart_host_write_str("PONG\r\n");
}
else if (strncmp(p_line, "USB_READY", 9U) == 0)
{
/* Проверка что USB CDC enumeration завершён и хост открыл порт. */
if (bsp_usb_cdc_is_ready())
{
bsp_uart_host_write_str("1\r\n");
}
else
{
bsp_uart_host_write_str("0\r\n");
}
}
else if (p_line[0] != '\0')
{
bsp_uart_host_write_str("ERR_UNKNOWN\r\n");
}
}
/* ---- USB CDC echo (тестируемый канал) ---- */
static void usb_cdc_echo_process(void)
{
static uint8_t s_usb_rx_buf[BSP_USB_CDC_MAX_PACKET_SIZE];
size_t n = bsp_usb_cdc_read(s_usb_rx_buf, sizeof(s_usb_rx_buf));
if ((n > 0U) && bsp_usb_cdc_write_ready())
{
bsp_usb_cdc_write(s_usb_rx_buf, n);
}
}
/* ---- main ---- */
int main(void)
{
board_hw_init();
bsp_tick_init();
bsp_led_init();
bsp_uart_host_init(CLI_BAUD_RATE);
/* Инициализация USB CDC — тестируемый модуль. */
bsp_usb_cdc_init();
bsp_led_on(LED_HEARTBEAT);
/* Шлём READY пока хост не открыл UART порт. */
while (bsp_uart_host_rx_available() == 0U)
{
bsp_uart_host_write_str("READY\r\n");
bsp_delay(200U);
}
static uint8_t s_line_buf[CLI_LINE_MAX];
for (;;)
{
/* 1. Обработка UART CLI команд. */
size_t len = cli_read_line(s_line_buf, sizeof(s_line_buf));
if (len > 0U)
{
cli_process_line((const char *) s_line_buf);
}
/* 2. USB CDC echo — всё что пришло по USB, отправляем обратно. */
usb_cdc_echo_process();
/* 3. Поллинг USB стека. */
bsp_usb_cdc_poll();
}
}

View file

@ -0,0 +1,103 @@
"""
05_test_usb_cdc.py HIL тест bsp_usb_cdc.
Схема:
UART (MCU-Link VCOM) управляющий канал (PING/PONG, USB_READY)
USB CDC (TFT Board) тестируемый канал (echo)
conftest.loaded_hil_usb_cdc pyocd_utils: load ELF
conftest.uart_hil_usb_cdc UART: ждать READY
conftest.usb_cdc_port USB CDC: открыть порт, установить DTR
Запуск:
just host::hil-usb-cdc
uv run --directory tools/hil pytest 05_test_usb_cdc.py -v
Переменные окружения:
HIL_USB_CDC_PORT порт USB CDC устройства (/dev/cu.usbmodemXXXX)
HIL_USB_CDC_BAUD baudrate (по умолчанию 115200)
"""
import time
import pytest
from conftest import uart_cmd
# Таймаут чтения USB CDC — достаточный для HS bulk transfer.
USB_CDC_READ_TIMEOUT_S = 1.0
def usb_cdc_echo(ser, data: bytes, timeout_s: float = USB_CDC_READ_TIMEOUT_S) -> bytes:
"""Отправить данные по USB CDC и прочитать echo."""
ser.reset_input_buffer()
ser.write(data)
ser.flush()
result = b""
deadline = time.monotonic() + timeout_s
while len(result) < len(data) and time.monotonic() < deadline:
chunk = ser.read(len(data) - len(result))
if chunk:
result += chunk
return result
@pytest.mark.usb_vcom
class TestUsbCdcBasic:
"""Базовые тесты USB CDC ACM — enumeration, echo, payload sizes."""
@pytest.fixture(autouse=True)
def _setup(self, uart_hil_usb_cdc, usb_cdc_port):
self.uart = uart_hil_usb_cdc
self.cdc = usb_cdc_port
# ---- UART control channel ----
def test_uart_ping(self):
"""UART канал работает — прошивка запущена."""
assert uart_cmd(self.uart, "PING") == "PONG"
def test_usb_ready(self):
"""USB CDC enumeration завершён, хост подключён."""
assert uart_cmd(self.uart, "USB_READY") == "1"
# ---- USB CDC echo ----
def test_echo_short(self):
"""Echo 5 байт — минимальный пакет."""
data = b"hello"
assert usb_cdc_echo(self.cdc, data) == data
def test_echo_with_newlines(self):
"""Echo с CR/LF — проверка что USB CDC бинарный, не line-based."""
data = b"line1\r\nline2\r\n"
assert usb_cdc_echo(self.cdc, data) == data
def test_echo_binary(self):
"""Echo бинарных данных — все значения 0x00..0xFF."""
data = bytes(range(256))
assert usb_cdc_echo(self.cdc, data) == data
def test_echo_repeated(self):
"""10 echo подряд — нет зависаний, нет потерь."""
for i in range(10):
data = f"packet_{i:03d}".encode()
result = usb_cdc_echo(self.cdc, data)
assert result == data, f"Сбой на итерации {i}: {result!r} != {data!r}"
def test_echo_64_bytes(self):
"""Echo 64 байт — граница FS bulk packet."""
data = b"A" * 64
assert usb_cdc_echo(self.cdc, data) == data
def test_echo_512_bytes(self):
"""Echo 512 байт — граница HS bulk packet (BSP_USB_CDC_MAX_PACKET_SIZE)."""
data = bytes([i & 0xFF for i in range(512)])
assert usb_cdc_echo(self.cdc, data) == data
# ---- Error recovery ----
def test_uart_after_cdc(self):
"""UART канал работает после серии USB CDC операций."""
assert uart_cmd(self.uart, "PING") == "PONG"

View file

@ -17,7 +17,7 @@ import serial
import env_config as cfg import env_config as cfg
from pyocd_utils import flexram_init, load_elf, open_target, run_from_vectors from pyocd_utils import flexram_init, load_elf, open_target, run_from_vectors
import os
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# Задержка после включения питания таргета (мс стабилизации + POR) # Задержка после включения питания таргета (мс стабилизации + POR)
@ -263,6 +263,13 @@ def loaded_hil_can(request: pytest.FixtureRequest, m5: M5Agent) -> None:
Path(cfg.BUILD_DIR) / "tests/target/hil_can/test_hil_can.elf", Path(cfg.BUILD_DIR) / "tests/target/hil_can/test_hil_can.elf",
) )
@pytest.fixture(scope="module")
def loaded_hil_usb_cdc(request: pytest.FixtureRequest, m5: M5Agent) -> None:
_load_elf(
request,
Path(cfg.BUILD_DIR) / "tests/target/hil_usb_cdc/test_hil_usb_cdc.elf",
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Фикстуры UART # Фикстуры UART
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -301,6 +308,56 @@ def uart_button(
ser = _open_uart_and_wait_ready(request) ser = _open_uart_and_wait_ready(request)
yield ser yield ser
ser.close() ser.close()
@pytest.fixture(scope="module")
def uart_hil_usb_cdc(
request: pytest.FixtureRequest,
loaded_hil_usb_cdc,
) -> Generator[serial.Serial, None, None]:
ser = _open_uart_and_wait_ready(request)
yield ser
ser.close()
# ---------------------------------------------------------------------------
# hil_usb_cdc — USB CDC ACM тест (два канала: UART + USB CDC)
# ---------------------------------------------------------------------------
@pytest.fixture(scope="module")
def usb_cdc_port(
uart_hil_usb_cdc,
) -> Generator[serial.Serial, None, None]:
"""Открыть USB CDC порт таргета. Ждёт появления порта и DTR ready."""
import time
port = os.environ.get("HIL_USB_CDC_PORT", "")
if not port:
pytest.skip("HIL_USB_CDC_PORT not set")
baud = int(os.environ.get("HIL_USB_CDC_BAUD", "115200"))
timeout_s = float(os.environ.get("HIL_USB_CDC_TIMEOUT", "5.0"))
# Ждём появления USB CDC порта (enumeration после загрузки ELF).
deadline = time.monotonic() + timeout_s
ser = None
while time.monotonic() < deadline:
try:
ser = serial.Serial(port, baud, timeout=0.5)
break
except serial.SerialException:
time.sleep(0.3)
if ser is None:
pytest.fail(f"USB CDC port {port} not available after {timeout_s}s")
# Установить DTR чтобы firmware увидела DTE presence.
ser.dtr = True
time.sleep(0.3)
# Сбросить входной буфер — могут быть мусорные байты от enumeration.
ser.reset_input_buffer()
yield ser
ser.close()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Утилита для тестов # Утилита для тестов
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -16,6 +16,7 @@ python_files = ["test_*.py", "*_test.py", "??_test_*.py"]
markers = [ markers = [
"gpio: тесты GPIO", "gpio: тесты GPIO",
"interactive: тест требует действий оператора", "interactive: тест требует действий оператора",
"usb_vcom: основной канал связи USB_CDC",
"uart: тесты UART CLI", "uart: тесты UART CLI",
"opto: тесты оптоизолированных входов", "opto: тесты оптоизолированных входов",
"m5: тесты с участием M5StampPLC", "m5: тесты с участием M5StampPLC",