diff --git a/.env.example b/.env.example index d99ca71..796c8bc 100644 --- a/.env.example +++ b/.env.example @@ -46,3 +46,7 @@ HIL_READY_TIMEOUT=5.0 HIL_M5_TIMEOUT=3.0 HIL_PYOCD_FREQUENCY=1000000 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 \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json index 8a3b6bc..8ef03f7 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -174,6 +174,7 @@ "test_host_uart", "test_hil_button", "test_hil_can", + "test_hil_usb_cdc", "test_hil_opto" ] } diff --git a/firmware/test/main.c b/firmware/test/main.c index a46b8a3..5b4568c 100644 --- a/firmware/test/main.c +++ b/firmware/test/main.c @@ -9,7 +9,7 @@ #include #include - +#include int main(void) { const uint16_t DELAY_MS = 100; @@ -38,6 +38,12 @@ int main(void) 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_led_toggle(LED_HEARTBEAT); bsp_delay(DELAY_MS); diff --git a/just/host.just b/just/host.just index 4703511..f097b4b 100644 --- a/just/host.just +++ b/just/host.just @@ -334,7 +334,7 @@ _hil_build := BUILD_DIR / "target-debug" [group('hil')] hil-run: 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-тесты: кнопки, дисплей')] [group('hil')] @@ -366,6 +366,12 @@ hil-can: HIL_BUILD_DIR={{ _hil_build }} \ 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) # diff --git a/tests/target/CMakeLists.txt b/tests/target/CMakeLists.txt index d79766f..cc90e1d 100644 --- a/tests/target/CMakeLists.txt +++ b/tests/target/CMakeLists.txt @@ -2,3 +2,4 @@ add_subdirectory(host_uart) add_subdirectory(hil_opto) add_subdirectory(hil_can) add_subdirectory(hil_button) +add_subdirectory(hil_usb_cdc) diff --git a/tests/target/hil_usb_cdc/CMakeLists.txt b/tests/target/hil_usb_cdc/CMakeLists.txt new file mode 100644 index 0000000..95742f0 --- /dev/null +++ b/tests/target/hil_usb_cdc/CMakeLists.txt @@ -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} $ + COMMENT "Size: ${TARGET_NAME}") diff --git a/tests/target/hil_usb_cdc/main.c b/tests/target/hil_usb_cdc/main.c new file mode 100644 index 0000000..c29f436 --- /dev/null +++ b/tests/target/hil_usb_cdc/main.c @@ -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 +#include +#include + +#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(); + } +} \ No newline at end of file diff --git a/tools/hil/05_test_usb_cdc.py b/tools/hil/05_test_usb_cdc.py new file mode 100644 index 0000000..4b63771 --- /dev/null +++ b/tools/hil/05_test_usb_cdc.py @@ -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" \ No newline at end of file diff --git a/tools/hil/conftest.py b/tools/hil/conftest.py index 3b8665f..7a1b6a4 100644 --- a/tools/hil/conftest.py +++ b/tools/hil/conftest.py @@ -17,7 +17,7 @@ import serial import env_config as cfg from pyocd_utils import flexram_init, load_elf, open_target, run_from_vectors - +import os log = logging.getLogger(__name__) # Задержка после включения питания таргета (мс стабилизации + 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", ) +@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 # --------------------------------------------------------------------------- @@ -301,6 +308,56 @@ def uart_button( ser = _open_uart_and_wait_ready(request) yield ser 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() + + # --------------------------------------------------------------------------- # Утилита для тестов # --------------------------------------------------------------------------- diff --git a/tools/hil/pyproject.toml b/tools/hil/pyproject.toml index 5b385b4..db0c0fc 100644 --- a/tools/hil/pyproject.toml +++ b/tools/hil/pyproject.toml @@ -16,6 +16,7 @@ python_files = ["test_*.py", "*_test.py", "??_test_*.py"] markers = [ "gpio: тесты GPIO", "interactive: тест требует действий оператора", + "usb_vcom: основной канал связи USB_CDC", "uart: тесты UART CLI", "opto: тесты оптоизолированных входов", "m5: тесты с участием M5StampPLC",