# fixes: pyformat + O1 release opt level

This commit is contained in:
Dmitry Akimov 2026-06-30 10:28:34 +03:00
parent 9be4a2aecc
commit bbd02f840e
15 changed files with 322 additions and 177 deletions

6
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff"
}
}

View file

@ -109,17 +109,17 @@ set(CMAKE_ASM_FLAGS_DEBUG
CACHE INTERNAL "")
set(CMAKE_C_FLAGS_RELEASE
"-O3 -DNDEBUG"
"-O1 -DNDEBUG"
CACHE INTERNAL "")
set(CMAKE_CXX_FLAGS_RELEASE
"-O3 -DNDEBUG"
"-O1 -DNDEBUG"
CACHE INTERNAL "")
set(CMAKE_ASM_FLAGS_RELEASE
""
CACHE INTERNAL "")
# -----------------------------------------------------------------------------
# C runtime библиотека newlib-nano (меньше размер, подходит для embedded)
# C runtime библиотека newlib-nano
# -----------------------------------------------------------------------------
# --specs=nano.specs использовать newlib-nano: облегчённая stdlib,
# меньший размер printf/malloc -Wl,--no-warn-rwx-segments подавить

View file

@ -64,4 +64,4 @@ class TestUartBasic:
def test_unknown_then_ping(self):
"""После неизвестной команды канал продолжает работать."""
uart_cmd(self.ser, "FOOBAR")
assert uart_cmd(self.ser, "PING") == "PONG"
assert uart_cmd(self.ser, "PING") == "PONG"

View file

@ -36,7 +36,7 @@ from conftest import uart_cmd
# process() цикл ~10 мс (CLI_RX_TIMEOUT)
# UART round-trip ~2 мс
# Итого минимум ~33 мс. Берём с запасом × 3.
RELAY_ON_S = 0.1 # ждать после включения реле
RELAY_ON_S = 0.1 # ждать после включения реле
# После выключения нужно убедиться что INACTIVE подтверждён дебаунсом.
# Берём чуть больше чем RELAY_ON_S — дебаунс работает симметрично.
@ -47,6 +47,7 @@ RELAY_OFF_S = 0.1
# Вспомогательные функции
# ---------------------------------------------------------------------------
def opto_read(ser, ch: int) -> str:
"""Прочитать состояние канала (1-based). Возвращает 'ACTIVE' или 'INACTIVE'."""
return uart_cmd(ser, f"OPTO_READ {ch}")
@ -87,6 +88,7 @@ def all_off_and_confirm(m5, ser, timeout_s: float = 1.0) -> None:
# Проверка каналов связи
# ---------------------------------------------------------------------------
class TestOptoConnectivity:
"""Базовая проверка: таргет и M5 отвечают."""
@ -108,6 +110,7 @@ class TestOptoConnectivity:
# Состояние по умолчанию (все реле выключены)
# ---------------------------------------------------------------------------
class TestOptoReadDefault:
"""Все каналы INACTIVE без внешнего воздействия."""
@ -132,6 +135,7 @@ class TestOptoReadDefault:
# Активация / деактивация каналов
# ---------------------------------------------------------------------------
class TestOptoActivateDeactivate:
"""Активация/деактивация каждого канала по отдельности через M5."""
@ -164,14 +168,16 @@ class TestOptoActivateDeactivate:
self.m5.opto_set(ch, True)
time.sleep(RELAY_ON_S)
for other in others:
assert opto_read(self.ser, other) == "INACTIVE", \
assert opto_read(self.ser, other) == "INACTIVE", (
f"ch{other} должен быть INACTIVE когда активен только ch{ch}"
)
# ---------------------------------------------------------------------------
# Callback-события (проверяет ISR + debounce + bsp_opto_process)
# ---------------------------------------------------------------------------
class TestOptoEvents:
"""
Проверяет что прерывание + bsp_opto_process() + коллбэк корректно
@ -233,4 +239,4 @@ class TestOptoEvents:
last = uart_cmd(self.ser, "OPTO_LAST_EVENT")
ch_str, _ = last.split()
assert ch_str == "2", f"Ожидали ch=2, получили {ch_str!r}"
assert ch_str == "2", f"Ожидали ch=2, получили {ch_str!r}"

View file

@ -37,7 +37,7 @@ from conftest import uart_cmd
# При 125 kbit/s один фрейм занимает ~0.1 мс.
# Добавляем запас на задержку USB CDC (M5 ↔ хост) и polling в прошивке.
CAN_SETTLE_S = 0.05 # ждать после отправки перед чтением
CAN_SETTLE_S = 0.05 # ждать после отправки перед чтением
# Таймаут CAN_RECV на таргете (мс) — передаётся в команду.
# Должен быть достаточным для round-trip через шину + USB CDC M5.
@ -51,6 +51,7 @@ CAN_RECV_EMPTY_MS = 100
# Вспомогательные функции
# ---------------------------------------------------------------------------
def can_send_m5(m5, can_id: int, data: list[int], ext: bool = False) -> None:
"""M5 отправляет CAN-фрейм на шину."""
m5.can_send(can_id, data, ext=ext)
@ -70,10 +71,10 @@ def can_send_target(ser, can_id: int, data: list[int], ext: bool = False) -> str
Таргет отправляет CAN-фрейм.
Возвращает ответ прошивки: 'OK', 'ERR_TIMEOUT', 'ERR_BUSY', 'ERR_PARAM'.
"""
dlc = len(data)
data_str = " ".join(str(b) for b in data)
ext_flag = 1 if ext else 0
cmd = f"CAN_SEND {can_id} {ext_flag} {dlc}"
dlc = len(data)
data_str = " ".join(str(b) for b in data)
ext_flag = 1 if ext else 0
cmd = f"CAN_SEND {can_id} {ext_flag} {dlc}"
if dlc > 0:
cmd += f" {data_str}"
return uart_cmd(ser, cmd)
@ -91,9 +92,9 @@ def can_recv_target(ser, timeout_ms: int = CAN_RECV_TIMEOUT_MS) -> dict | None:
if len(parts) < 3:
raise ValueError(f"Неожиданный ответ CAN_RECV: {resp!r}")
can_id = int(parts[0])
ext = bool(int(parts[1]))
dlc = int(parts[2])
data = [int(b) for b in parts[3:3 + dlc]]
ext = bool(int(parts[1]))
dlc = int(parts[2])
data = [int(b) for b in parts[3 : 3 + dlc]]
return {"id": can_id, "ext": ext, "dlc": dlc, "data": data}
@ -112,13 +113,14 @@ def reset_events(ser) -> None:
# Проверка каналов связи
# ---------------------------------------------------------------------------
class TestCanConnectivity:
"""Базовая проверка: таргет и M5 отвечают."""
@pytest.fixture(autouse=True)
def _setup(self, uart_can, m5):
self.ser = uart_can
self.m5 = m5
self.m5 = m5
def test_target_ping(self):
"""PING → PONG: UART-канал host↔target работает."""
@ -131,20 +133,23 @@ class TestCanConnectivity:
def test_m5_can_available(self):
"""M5 сообщает что CAN инициализирован (can_ok=true в info)."""
info = self.m5.info()
assert info.get("can_ok"), "M5 CAN не инициализирован — проверьте трансивер и agent.py"
assert info.get("can_ok"), (
"M5 CAN не инициализирован — проверьте трансивер и agent.py"
)
# ---------------------------------------------------------------------------
# M5 → таргет
# ---------------------------------------------------------------------------
class TestCanM5ToTarget:
"""M5 отправляет фрейм — таргет принимает."""
@pytest.fixture(autouse=True)
def _setup(self, uart_can, m5):
self.ser = uart_can
self.m5 = m5
self.m5 = m5
uart_cmd(self.ser, "CAN_ACCEPT_ALL")
reset_events(self.ser)
@ -153,10 +158,10 @@ class TestCanM5ToTarget:
payload = [0x11, 0x22, 0x33]
can_send_m5(self.m5, 0x123, payload)
frame = can_recv_target(self.ser)
assert frame is not None, "Таргет не принял фрейм от M5"
assert frame["id"] == 0x123, f"Неверный ID: {frame['id']:#x}"
assert frame["ext"] is False, "Ожидали STD-фрейм"
assert frame["dlc"] == 3, f"Неверный DLC: {frame['dlc']}"
assert frame is not None, "Таргет не принял фрейм от M5"
assert frame["id"] == 0x123, f"Неверный ID: {frame['id']:#x}"
assert frame["ext"] is False, "Ожидали STD-фрейм"
assert frame["dlc"] == 3, f"Неверный DLC: {frame['dlc']}"
assert frame["data"] == payload, f"Неверные данные: {frame['data']}"
def test_ext_frame_received(self):
@ -164,10 +169,10 @@ class TestCanM5ToTarget:
payload = [0xAA, 0xBB]
can_send_m5(self.m5, 0x1ABCDEF, payload, ext=True)
frame = can_recv_target(self.ser)
assert frame is not None, "Таргет не принял EXT-фрейм"
assert frame["id"] == 0x1ABCDEF, f"Неверный EXT ID: {frame['id']:#x}"
assert frame["ext"] is True, "Ожидали EXT-фрейм"
assert frame["data"] == payload, f"Неверные данные: {frame['data']}"
assert frame is not None, "Таргет не принял EXT-фрейм"
assert frame["id"] == 0x1ABCDEF, f"Неверный EXT ID: {frame['id']:#x}"
assert frame["ext"] is True, "Ожидали EXT-фрейм"
assert frame["data"] == payload, f"Неверные данные: {frame['data']}"
def test_max_dlc_frame(self):
"""M5 TX 8-байтовый фрейм — таргет принимает все 8 байт корректно."""
@ -175,7 +180,7 @@ class TestCanM5ToTarget:
can_send_m5(self.m5, 0x7FF, payload)
frame = can_recv_target(self.ser)
assert frame is not None
assert frame["dlc"] == 8
assert frame["dlc"] == 8
assert frame["data"] == payload
def test_zero_dlc_frame(self):
@ -183,7 +188,7 @@ class TestCanM5ToTarget:
can_send_m5(self.m5, 0x001, [])
frame = can_recv_target(self.ser)
assert frame is not None
assert frame["dlc"] == 0
assert frame["dlc"] == 0
assert frame["data"] == []
def test_rx_event_counter_increments(self):
@ -200,21 +205,22 @@ class TestCanM5ToTarget:
# Таргет → M5
# ---------------------------------------------------------------------------
class TestCanTargetToM5:
"""Таргет отправляет фрейм — M5 принимает."""
@pytest.fixture(autouse=True)
def _setup(self, uart_can, m5):
self.ser = uart_can
self.m5 = m5
self.m5 = m5
def test_std_frame_sent(self):
"""Таргет TX STD → M5 принимает с верным ID и данными."""
payload = [0xDE, 0xAD, 0xBE, 0xEF]
assert can_send_target(self.ser, 0x456, payload) == "OK"
frame = can_recv_m5(self.m5)
assert frame["id"] == 0x456, f"Неверный ID: {frame['id']:#x}"
assert frame["ext"] is False, "Ожидали STD-фрейм"
assert frame["id"] == 0x456, f"Неверный ID: {frame['id']:#x}"
assert frame["ext"] is False, "Ожидали STD-фрейм"
assert frame["data"] == payload, f"Неверные данные: {frame['data']}"
def test_ext_frame_sent(self):
@ -222,8 +228,8 @@ class TestCanTargetToM5:
payload = [0x01, 0x02]
assert can_send_target(self.ser, 0x1FFFFFF, payload, ext=True) == "OK"
frame = can_recv_m5(self.m5)
assert frame["id"] == 0x1FFFFFF, f"Неверный EXT ID: {frame['id']:#x}"
assert frame["ext"] is True, "Ожидали EXT-фрейм"
assert frame["id"] == 0x1FFFFFF, f"Неверный EXT ID: {frame['id']:#x}"
assert frame["ext"] is True, "Ожидали EXT-фрейм"
def test_max_std_id(self):
"""Максимальный достижимый STD ID через M5 recv — проверяем 0x7FE."""
@ -249,14 +255,16 @@ class TestCanTargetToM5:
payload = [0x00, 0xFF, 0x55, 0xAA, 0x0F, 0xF0, 0x01, 0xFE]
assert can_send_target(self.ser, 0x300, payload) == "OK"
frame = can_recv_m5(self.m5)
assert frame["data"] == payload, \
assert frame["data"] == payload, (
f"Данные искажены: ожидали {payload}, получили {frame['data']}"
)
# ---------------------------------------------------------------------------
# Фильтрация
# ---------------------------------------------------------------------------
class TestCanFiltering:
"""
Проверка фильтрации по ID: только нужные фреймы проходят,
@ -266,7 +274,7 @@ class TestCanFiltering:
@pytest.fixture(autouse=True)
def _setup(self, uart_can, m5):
self.ser = uart_can
self.m5 = m5
self.m5 = m5
reset_events(self.ser)
def test_accept_all_receives_any_id(self):
@ -288,24 +296,27 @@ class TestCanFiltering:
can_send_m5(self.m5, target_id, [0xAA])
frame = can_recv_target(self.ser)
assert frame is not None, "Фрейм с нужным ID не принят"
assert frame["id"] == target_id, f"Получен ID {frame['id']:#x}, ожидали {target_id:#x}"
assert frame is not None, "Фрейм с нужным ID не принят"
assert frame["id"] == target_id, (
f"Получен ID {frame['id']:#x}, ожидали {target_id:#x}"
)
def test_filter_wrong_id_blocked(self):
"""set_filter с точной маской блокирует другой ID."""
target_id = 0x123
wrong_id = 0x456
wrong_id = 0x456
assert set_filter_target(self.ser, 0, target_id, 0x7FF) == "OK"
can_send_m5(self.m5, wrong_id, [0xBB])
frame = can_recv_target(self.ser, timeout_ms=CAN_RECV_EMPTY_MS)
assert frame is None, \
assert frame is None, (
f"Фрейм с ID {wrong_id:#x} прошёл фильтр, хотя не должен был"
)
def test_filter_mask_passes_group(self):
"""Маска 0x7F0 пропускает группу ID с одинаковыми старшими битами."""
base_id = 0x120
mask = 0x7F0 # проверять биты 11..4, биты 3..0 — игнорировать
mask = 0x7F0 # проверять биты 11..4, биты 3..0 — игнорировать
assert set_filter_target(self.ser, 0, base_id, mask) == "OK"
@ -327,8 +338,8 @@ class TestCanFiltering:
can_send_m5(self.m5, target_id, [0x42], ext=True)
frame = can_recv_target(self.ser)
assert frame is not None, "EXT-фрейм с нужным ID не принят"
assert frame["id"] == target_id
assert frame is not None, "EXT-фрейм с нужным ID не принят"
assert frame["id"] == target_id
assert frame["ext"] is True
def test_accept_all_after_filter(self):
@ -353,13 +364,14 @@ class TestCanFiltering:
# STD и EXT раздельно
# ---------------------------------------------------------------------------
class TestCanFrameTypes:
"""STD и EXT фреймы не перепутываются — is_extended корректен."""
@pytest.fixture(autouse=True)
def _setup(self, uart_can, m5):
self.ser = uart_can
self.m5 = m5
self.m5 = m5
uart_cmd(self.ser, "CAN_ACCEPT_ALL")
def test_std_frame_is_not_ext(self):
@ -374,15 +386,15 @@ class TestCanFrameTypes:
can_send_m5(self.m5, 0x1FF, [0x01], ext=True)
frame = can_recv_target(self.ser)
assert frame is not None
assert frame["ext"] is True, "EXT-фрейм ошибочно помечен как STD"
assert frame["ext"] is True, "EXT-фрейм ошибочно помечен как STD"
def test_std_id_range_boundary(self):
"""Граничные STD ID: 0x000 и 0x7FF."""
for can_id in [0x000, 0x7FF]:
can_send_m5(self.m5, can_id, [0xBB])
frame = can_recv_target(self.ser)
assert frame is not None, f"ID {can_id:#x} не принят"
assert frame["id"] == can_id, f"ID {frame['id']:#x}{can_id:#x}"
assert frame is not None, f"ID {can_id:#x} не принят"
assert frame["id"] == can_id, f"ID {frame['id']:#x}{can_id:#x}"
assert frame["ext"] is False
def test_ext_id_range_boundary(self):
@ -390,6 +402,6 @@ class TestCanFrameTypes:
for can_id in [0x000, 0x1FFFFFFF]:
can_send_m5(self.m5, can_id, [0xCC], ext=True)
frame = can_recv_target(self.ser)
assert frame is not None, f"EXT ID {can_id:#x} не принят"
assert frame is not None, f"EXT ID {can_id:#x} не принят"
assert frame["id"] == can_id, f"EXT ID {frame['id']:#x}{can_id:#x}"
assert frame["ext"] is True
assert frame["ext"] is True

View file

@ -25,9 +25,9 @@ def _operator_prompt(msg: str) -> None:
input(f"\n >>> {msg}\n Нажмите Enter когда готово...")
time.sleep(DEBOUNCE_SETTLE_S)
@pytest.mark.interactive
class TestHilButton:
@pytest.fixture(autouse=True)
def _setup(self, uart_button):
self.ser = uart_button
@ -133,4 +133,4 @@ class TestHilButton:
_operator_prompt("Нажми и УДЕРЖИ ТОЛЬКО кнопку 2 (TactBut2), кнопка 1 свободна")
assert uart_cmd(self.ser, "STATE 0") == "0", "Кнопка 1 не должна быть нажата"
assert uart_cmd(self.ser, "STATE 1") == "1", "Кнопка 2 должна быть нажата"
_operator_prompt("ОТПУСТИ кнопку 2 (TactBut2)")
_operator_prompt("ОТПУСТИ кнопку 2 (TactBut2)")

View file

@ -43,6 +43,7 @@ def usb_cdc_echo(ser, data: bytes, timeout_s: float = USB_CDC_READ_TIMEOUT_S) ->
return result
@pytest.mark.usb_vcom
class TestUsbCdcBasic:
"""Базовые тесты USB CDC ACM — enumeration, echo, payload sizes."""
@ -100,4 +101,4 @@ class TestUsbCdcBasic:
def test_uart_after_cdc(self):
"""UART канал работает после серии USB CDC операций."""
assert uart_cmd(self.uart, "PING") == "PONG"
assert uart_cmd(self.uart, "PING") == "PONG"

View file

@ -20,14 +20,15 @@ import pytest
from conftest import FirmwareCdc, M5Agent
# CAN параметры — должны совпадать с test_can.c
CAN_RX_ID = 0x100
CAN_RX_ID = 0x100
CAN_RX_DATA = [0xDE, 0xAD, 0xBE, 0xEF]
CAN_TX_ID = 0x200
CAN_TX_ID = 0x200
CAN_TX_DATA = [0xCA, 0xFE, 0xBA, 0xBE]
# Таймаут для M5 can_recv — с запасом относительно CAN_RX_TIMEOUT_MS (500 мс) в прошивке
CAN_RECV_TIMEOUT_MS = 1000
@pytest.mark.usb_vcom
class TestFirmwareCan:
"""Тест CAN-интерфейса через firmware_test CDC (протокол v2)."""
@ -37,7 +38,6 @@ class TestFirmwareCan:
self.cdc = firmware_cdc
self.m5 = m5
def _on_confirm(self, confirm_id: str) -> bool:
"""
Оркестратор CAN-теста.
@ -50,7 +50,7 @@ class TestFirmwareCan:
Таргет уже отправил фрейм ДО confirm_request.
M5 принимает фрейм, проверяет id+data, возвращает результат.
"""
if confirm_id == "can_rx_ready":
self.m5.can_send(CAN_RX_ID, CAN_RX_DATA)
# Небольшая пауза чтобы фрейм успел уйти на шину
@ -61,12 +61,15 @@ class TestFirmwareCan:
try:
frame = self.m5.can_recv(timeout_ms=CAN_RECV_TIMEOUT_MS)
import logging
logging.getLogger(__name__).info(
"M5 received frame: id=0x%X data=%s",
frame.get("id", -1), frame.get("data", [])
"M5 received frame: id=0x%X data=%s",
frame.get("id", -1),
frame.get("data", []),
)
except (TimeoutError, RuntimeError) as e:
import logging
logging.getLogger(__name__).warning("M5 can_recv failed: %s", e)
return False
@ -90,7 +93,6 @@ class TestFirmwareCan:
def test_ping(self) -> None:
"""Базовая проверка CDC-канала."""
self.cdc.ping()
def test_can_pass(self) -> None:
"""
@ -104,8 +106,7 @@ class TestFirmwareCan:
)
assert result.get("status") == "pass", (
f"test_can вернул {result.get('status')!r}: "
f"{result.get('detail', '')}"
f"test_can вернул {result.get('status')!r}: {result.get('detail', '')}"
)
def test_can_rx_fail_no_frame(self) -> None:
@ -131,6 +132,4 @@ class TestFirmwareCan:
f"Ожидали fail, получили {result.get('status')!r}"
)
detail = result.get("detail", "")
assert "no frame" in detail, (
f"detail должен содержать 'no frame': {detail!r}"
)
assert "no frame" in detail, f"detail должен содержать 'no frame': {detail!r}"

View file

@ -25,20 +25,21 @@ import pytest
from conftest import FirmwareCdc, M5Agent
RELAY_ON_S = 0.1 # реле замыкается быстро
RELAY_OFF_S = 0.25 # размыкание + дебаунс прошивки с запасом
RELAY_ON_S = 0.1 # реле замыкается быстро
RELAY_OFF_S = 0.25 # размыкание + дебаунс прошивки с запасом
# Маппинг confirm_id → (opto_ch, state)
_OPTO_ACTIONS: dict[str, tuple[int, bool]] = {
"opto_in1_active": (1, True),
"opto_in1_active": (1, True),
"opto_in1_inactive": (1, False),
"opto_in2_active": (2, True),
"opto_in2_active": (2, True),
"opto_in2_inactive": (2, False),
"opto_rs_active": (3, True),
"opto_rs_inactive": (3, False),
"opto_rs_active": (3, True),
"opto_rs_inactive": (3, False),
}
@pytest.mark.usb_vcom
class TestFirmwareOpto:
"""Тест оптовходов через firmware_test CDC (протокол v2)."""
@ -86,8 +87,7 @@ class TestFirmwareOpto:
)
assert result.get("status") == "pass", (
f"test_opto вернул {result.get('status')!r}: "
f"{result.get('detail', '')}"
f"test_opto вернул {result.get('status')!r}: {result.get('detail', '')}"
)
def test_opto_in1_fail_on_inactive(self) -> None:
@ -111,6 +111,4 @@ class TestFirmwareOpto:
f"Ожидали fail, получили {result.get('status')!r}"
)
detail = result.get("detail", "")
assert "mismatch" in detail, (
f"detail должен содержать 'mismatch': {detail!r}"
)
assert "mismatch" in detail, f"detail должен содержать 'mismatch': {detail!r}"

View file

@ -25,10 +25,10 @@ log = logging.getLogger(__name__)
# CLI-опции pytest (перекрывают .env и os.environ)
# ---------------------------------------------------------------------------
def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption("--elf", default=None, help="Путь к .elf файлу")
parser.addoption("--vcom", default=cfg.VCOM_PORT, help="VCOM-порт MCU-Link")
parser.addoption("--m5-port", default=None, help="M5StampPLC serial port")
parser.addoption("--no-load", action="store_true", help="ELF уже запущен")
parser.addoption("--elf", default=None, help="Путь к .elf файлу")
parser.addoption("--vcom", default=cfg.VCOM_PORT, help="VCOM-порт MCU-Link")
parser.addoption("--m5-port", default=None, help="M5StampPLC serial port")
parser.addoption("--no-load", action="store_true", help="ELF уже запущен")
# ---------------------------------------------------------------------------
@ -105,9 +105,7 @@ class M5Agent:
except json.JSONDecodeError:
continue
if not resp.get("ok"):
raise RuntimeError(
f"M5 error: {resp.get('err', '?')} (cmd={command})"
)
raise RuntimeError(f"M5 error: {resp.get('err', '?')} (cmd={command})")
return resp
raise TimeoutError(f"M5: нет ответа на '{command}'")
@ -124,14 +122,15 @@ class M5Agent:
def opto_all_off(self) -> None:
self.cmd("opto_all_off")
def info(self) -> dict:
"""Запросить информацию об агенте (включая can_ok)."""
return self.cmd("info")
def can_send(self, can_id: int, data: list, ext: bool = False) -> None:
"""Отправить CAN-фрейм с шины M5."""
self.cmd("can_send", id=can_id, data=list(data), ext=ext)
def can_recv(self, timeout_ms: int = 500) -> dict:
"""
Принять CAN-фрейм на M5.
@ -205,11 +204,12 @@ def m5(request: pytest.FixtureRequest) -> Generator[M5Agent, None, None]:
# Фикстуры загрузки (scope=module — один раз на файл с тестами)
# ---------------------------------------------------------------------------
@pytest.fixture(scope="module")
def loaded_host_uart(request: pytest.FixtureRequest, m5: M5Agent) -> None:
"""
Загрузить test_hil_button.elf на таргет по SWD
Явная зависимость от m5 гарантирует порядок:
1. m5 создаётся первым питание таргета включено
2. только потом pyOCD подключается и грузит ELF
@ -219,11 +219,12 @@ def loaded_host_uart(request: pytest.FixtureRequest, m5: M5Agent) -> None:
Path(cfg.BUILD_DIR) / "tests/target/host_uart/test_host_uart.elf",
)
@pytest.fixture(scope="module")
def loaded_hil_button(request: pytest.FixtureRequest, m5: M5Agent) -> None:
"""
Загрузить test_hil_button.elf на таргет по SWD
Явная зависимость от m5 гарантирует порядок:
1. m5 создаётся первым питание таргета включено
2. только потом pyOCD подключается и грузит ELF
@ -233,6 +234,7 @@ def loaded_hil_button(request: pytest.FixtureRequest, m5: M5Agent) -> None:
Path(cfg.BUILD_DIR) / "tests/target/hil_button/test_hil_button.elf",
)
@pytest.fixture(scope="module")
def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None:
"""
@ -249,11 +251,12 @@ def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None:
Path(cfg.BUILD_DIR) / "tests/target/hil_opto/test_hil_opto.elf",
)
@pytest.fixture(scope="module")
def loaded_hil_can(request: pytest.FixtureRequest, m5: M5Agent) -> None:
"""
Загрузить test_hil_can.elf на таргет по SWD
Явная зависимость от m5 гарантирует порядок:
1. m5 создаётся первым питание таргета включено
2. только потом pyOCD подключается и грузит ELF
@ -263,11 +266,12 @@ 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:
"""
Загрузить test_hil_usb_cdc.elf на таргет по SWD
Явная зависимость от m5 гарантирует порядок:
1. m5 создаётся первым питание таргета включено
2. только потом pyOCD подключается и грузит ELF
@ -277,29 +281,38 @@ def loaded_hil_usb_cdc(request: pytest.FixtureRequest, m5: M5Agent) -> None:
Path(cfg.BUILD_DIR) / "tests/target/hil_usb_cdc/test_hil_usb_cdc.elf",
)
# ---------------------------------------------------------------------------
# Фикстуры UART (установление соединения с VCOM программатора NXP MCU-Link)
# Выполняется один раз на тест
# ---------------------------------------------------------------------------
_UART_FIXTURE_MAP = {
"uart": "loaded_host_uart",
"uart_opto": "loaded_hil_opto",
"uart_can": "loaded_hil_can",
"uart_button": "loaded_hil_button",
"uart": "loaded_host_uart",
"uart_opto": "loaded_hil_opto",
"uart_can": "loaded_hil_can",
"uart_button": "loaded_hil_button",
"uart_hil_usb_cdc": "loaded_hil_usb_cdc",
}
def _make_uart_fixture(loaded_name: str):
@pytest.fixture(scope="module")
def _fixture(request: pytest.FixtureRequest) -> Generator[serial.Serial, None, None]:
def _fixture(
request: pytest.FixtureRequest,
) -> Generator[serial.Serial, None, None]:
request.getfixturevalue(loaded_name) # триггерит зависимость явно
port = request.config.getoption("--vcom")
with _uart_context(port, cfg.VCOM_BAUD, cfg.READY_TIMEOUT) as ser:
yield ser
return _fixture
# Регистрируем все фикстуры в пространстве имён модуля одной строкой
for _name, _dep in _UART_FIXTURE_MAP.items():
globals()[_name] = _make_uart_fixture(_dep)
# ---------------------------------------------------------------------------
# Фикстуры USB CDC (открытие порта, установка DTR)
# ---------------------------------------------------------------------------
@ -318,13 +331,17 @@ def usb_cdc_port(
ser = None
while time.monotonic() < deadline:
try:
ser = serial.Serial(port=cfg.TARGET_VCOM_PORT, baudrate=cfg.TARGET_VCOM_BAUD, timeout=0.5)
ser = serial.Serial(
port=cfg.TARGET_VCOM_PORT, baudrate=cfg.TARGET_VCOM_BAUD, timeout=0.5
)
break
except serial.SerialException:
time.sleep(0.3)
if ser is None:
pytest.fail(f"USB CDC port {cfg.TARGET_VCOM_PORT} not available after {cfg.TARGET_VCOM_TIMEOUT}s")
pytest.fail(
f"USB CDC port {cfg.TARGET_VCOM_PORT} not available after {cfg.TARGET_VCOM_TIMEOUT}s"
)
try:
# Установить DTR чтобы firmware увидела DTE presence.
@ -334,7 +351,7 @@ def usb_cdc_port(
ser.reset_input_buffer()
yield ser
finally:
ser.close()
ser.close()
# ---------------------------------------------------------------------------
@ -354,6 +371,7 @@ def uart_cmd(ser: serial.Serial, cmd: str) -> str:
# firmware_test CDC — фикстура для тестирования firmware_test через протокол v2
# ---------------------------------------------------------------------------
class FirmwareCdc:
"""
Драйвер firmware_test протокола v2 (JSON-lines через USB CDC).
@ -395,7 +413,9 @@ class FirmwareCdc:
msg = self._readline(timeout_s=max(remaining, 0.1))
if msg.get("type") == event_type:
return msg
log.debug("firmware_cdc: пропускаем %r (ждём %r)", msg.get("type"), event_type)
log.debug(
"firmware_cdc: пропускаем %r (ждём %r)", msg.get("type"), event_type
)
raise TimeoutError(
f"firmware_cdc: событие {event_type!r} не получено за {timeout_s} с"
)
@ -504,8 +524,7 @@ def firmware_cdc(m5: M5Agent) -> Generator[FirmwareCdc, None, None]:
if ser is None:
pytest.fail(
f"CDC порт {cfg.TARGET_VCOM_PORT} недоступен "
f"за {cfg.TARGET_VCOM_TIMEOUT} с"
f"CDC порт {cfg.TARGET_VCOM_PORT} недоступен за {cfg.TARGET_VCOM_TIMEOUT} с"
)
try:
@ -528,4 +547,4 @@ def firmware_cdc(m5: M5Agent) -> Generator[FirmwareCdc, None, None]:
yield cdc
finally:
ser.close()
ser.close()

View file

@ -14,28 +14,28 @@ from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[2]
def _get(key: str, default: str) -> str:
return os.environ.get(key, default)
BUILD_DIR: str = _get("HIL_BUILD_DIR",
str(_REPO_ROOT / "build" / "target-debug"))
PYOCD_FREQUENCY: int = int(_get("HIL_PYOCD_FREQUENCY", "1000000"))
BUILD_DIR: str = _get("HIL_BUILD_DIR", str(_REPO_ROOT / "build" / "target-debug"))
PYOCD_FREQUENCY: int = int(_get("HIL_PYOCD_FREQUENCY", "1000000"))
POWER_ON_SETTLE_S = float(_get("HIL_TARGET_POWER_SETTLE_S", "1.5"))
# VCOM на программаторе NXP MCU-Link
VCOM_PORT: str = _get("HIL_VCOM_PORT", "/dev/ttyACM0")
VCOM_BAUD: int = int(_get("HIL_VCOM_BAUD", "115200"))
READY_TIMEOUT: float = float(_get("HIL_READY_TIMEOUT", "5.0"))
VCOM_PORT: str = _get("HIL_VCOM_PORT", "/dev/ttyACM0")
VCOM_BAUD: int = int(_get("HIL_VCOM_BAUD", "115200"))
READY_TIMEOUT: float = float(_get("HIL_READY_TIMEOUT", "5.0"))
# M5Stack StamPLC (промежуточная платформа для HIL)
M5_PORT: str = _get("HIL_M5_PORT", "/dev/ttyACM1")
M5_BAUD: int = int(_get("HIL_M5_BAUD", "115200"))
M5_TIMEOUT: float = float(_get("HIL_M5_TIMEOUT", "3.0"))
M5_PORT: str = _get("HIL_M5_PORT", "/dev/ttyACM1")
M5_BAUD: int = int(_get("HIL_M5_BAUD", "115200"))
M5_TIMEOUT: float = float(_get("HIL_M5_TIMEOUT", "3.0"))
# VCOM на плате таргета
TARGET_VCOM_PORT:str = _get("HIL_USB_CDC_PORT", "/dev/ttyACM2")
TARGET_VCOM_BAUD:int = int(_get("HIL_USB_CDC_BAUD", "115200"))
TARGET_VCOM_TIMEOUT:float = float(_get("HIL_USB_CDC_TIMEOUT", "3.0"))
TARGET_VCOM_PORT: str = _get("HIL_USB_CDC_PORT", "/dev/ttyACM2")
TARGET_VCOM_BAUD: int = int(_get("HIL_USB_CDC_BAUD", "115200"))
TARGET_VCOM_TIMEOUT: float = float(_get("HIL_USB_CDC_TIMEOUT", "3.0"))

View file

@ -20,4 +20,4 @@ logging.basicConfig(
if __name__ == "__main__":
elf = sys.argv[1] if len(sys.argv) > 1 else "firmware_test.elf"
load_and_run(elf)
print("Done — смотри на LED!")
print("Done — смотри на LED!")

View file

@ -126,4 +126,4 @@ def load_and_run(elf_path: str, frequency: int = 1_000_000) -> None:
with open_target(frequency) as target:
flexram_init(target)
load_elf(target, elf_path)
run_from_vectors(target)
run_from_vectors(target)

View file

@ -48,16 +48,18 @@ REPO_ROOT = Path(__file__).resolve().parents[2]
# ── Конфигурация из окружения (just экспортирует .env через set export) ───────
def _env(key: str, default: str) -> str:
return os.environ.get(key, default)
PYOCD_TARGET = _env("PYOCD_TARGET", "mimxrt1050_quadspi")
PYOCD_TARGET = _env("PYOCD_TARGET", "mimxrt1050_quadspi")
PYOCD_FREQUENCY = _env("PYOCD_FREQUENCY", "4000000")
BUILD_DIR = Path(_env("BUILD_DIR", str(REPO_ROOT / "build")))
FCB_PATH = Path(_env("FCB_PATH", str(REPO_ROOT / "tools/host/dcd/w25q128_fdcb.bin")))
BUILD_DIR = Path(_env("BUILD_DIR", str(REPO_ROOT / "build")))
FCB_PATH = Path(_env("FCB_PATH", str(REPO_ROOT / "tools/host/dcd/w25q128_fdcb.bin")))
if not FCB_PATH.is_absolute():
FCB_PATH = REPO_ROOT / FCB_PATH
HIL_DIR = REPO_ROOT / "tools" / "hil"
HIL_DIR = REPO_ROOT / "tools" / "hil"
# IVT располагается по смещению 0x1000 от начала Flash (ivtOffset в HAB yaml)
IVT_OFFSET = 0x1000
@ -65,12 +67,13 @@ IVT_OFFSET = 0x1000
# Маппинг имён прошивок → имена HAB-файлов (генерируются build::hab-*)
HAB_NAMES = {
"firmware_test": "firmware_test_hab.bin",
"bootloader": "bootloader_hab.bin",
"app": "app_hab.bin",
"bootloader": "bootloader_hab.bin",
"app": "app_hab.bin",
}
# ── Helpers ───────────────────────────────────────────────────────────────────
def build_full_image(fcb_path: Path, hab_path: Path) -> bytes:
"""Объединить FCB + padding + HAB в единый образ для записи с 0x60000000."""
fcb = fcb_path.read_bytes()
@ -82,7 +85,7 @@ def build_full_image(fcb_path: Path, hab_path: Path) -> bytes:
)
padding = b"\xff" * (IVT_OFFSET - len(fcb)) # 0xFF = erased flash value
image = fcb + padding + hab
image = fcb + padding + hab
print(f" FCB: {len(fcb):>6} bytes @ 0x60000000")
print(f" Padding: {len(padding):>6} bytes @ 0x{0x60000000 + len(fcb):08X}")
@ -95,13 +98,20 @@ def build_full_image(fcb_path: Path, hab_path: Path) -> bytes:
def run_pyocd_flash(image_path: Path, target: str, frequency: str) -> int:
"""Запустить pyocd flash через uv run из tools/hil."""
cmd = [
"uv", "run",
"--directory", str(HIL_DIR),
"pyocd", "flash",
"--target", target,
"--frequency", frequency,
"--base-address", "0x60000000",
"--erase", "sector",
"uv",
"run",
"--directory",
str(HIL_DIR),
"pyocd",
"flash",
"--target",
target,
"--frequency",
frequency,
"--base-address",
"0x60000000",
"--erase",
"sector",
str(image_path),
]
print(f"\n Running: {' '.join(cmd)}\n")
@ -110,10 +120,11 @@ def run_pyocd_flash(image_path: Path, target: str, frequency: str) -> int:
# ── Main ──────────────────────────────────────────────────────────────────────
def main() -> int:
parser = argparse.ArgumentParser(
description="Flash MIMXRT1052 via SWD (MCU-Link). "
"Prepends FCB to HAB image before programming.",
"Prepends FCB to HAB image before programming.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
@ -170,7 +181,7 @@ def main() -> int:
if not hab_path.exists():
print(f" ❌ HAB image not found: {hab_path}", file=sys.stderr)
fw_slug = args.firmware.replace("_", "-")
bt_slug = args.build_type.lower()
bt_slug = args.build_type.lower()
print(
f" Hint: run just build::hab-{fw_slug}-{bt_slug} "
"inside devcontainer first.",
@ -216,4 +227,4 @@ def main() -> int:
if __name__ == "__main__":
sys.exit(main())
sys.exit(main())

View file

@ -35,38 +35,40 @@ import time
from pathlib import Path
# ─── Пути ─────────────────────────────────────────────────────────────────────
SCRIPT_DIR = Path(__file__).parent.resolve()
REPO_ROOT = SCRIPT_DIR.parent.parent
SCRIPT_DIR = Path(__file__).parent.resolve()
REPO_ROOT = SCRIPT_DIR.parent.parent
FLASHLOADER = SCRIPT_DIR / "dcd" / "ivt_flashloader.bin"
# BUILD_DIR: берём из окружения (just экспортирует из .env как абсолютный путь),
# fallback — рассчитываем от расположения скрипта
BUILD_DIR = Path(os.environ.get("BUILD_DIR", str(REPO_ROOT / "build")))
# ─── USB VID:PID — из окружения (.env → just set export → uv run) ─────────────
def _usb(vid_key: str, vid_default: str, pid_key: str, pid_default: str) -> str:
vid = os.environ.get(vid_key, vid_default).strip().upper().lstrip("0X")
pid = os.environ.get(pid_key, pid_default).strip().upper().lstrip("0X")
return f"0x{vid},0x{pid}"
SDP_USB = _usb("BOOTROM_VID", "1fc9", "BOOTROM_PID", "0130") # BootROM SDP
BLHOST_USB = _usb("FLASHLOADER_VID","15a2", "FLASHLOADER_PID","0073") # Flashloader
SDP_USB = _usb("BOOTROM_VID", "1fc9", "BOOTROM_PID", "0130") # BootROM SDP
BLHOST_USB = _usb("FLASHLOADER_VID", "15a2", "FLASHLOADER_PID", "0073") # Flashloader
# ─── Аппаратные константы (часть логики прошивки, не конфигурация) ────────────
# FlexSPI NOR config option word: 0xC0000007
# bits[31:28]=0xC — tag (QuadSPI NOR)
# bits[3:0]=0x7 — option size
FLEXSPI_OPTION_ADDR = "0x2000"
FLEXSPI_OPTION_ADDR = "0x2000"
FLEXSPI_OPTION_VALUE = "0xC0000007"
FLEXSPI_MEMORY_ID = "9" # FlexSPI NOR memory interface ID
FLEXSPI_MEMORY_ID = "9" # FlexSPI NOR memory interface ID
# Option word для записи FCB: tag=0xF → Write FCB command
FLEXSPI_FCB_VALUE = "0xF000000F"
FLEXSPI_FCB_VALUE = "0xF000000F"
# Flash layout
FLASH_BASE = 0x60000000
HAB_OFFSET = 0x1000 # IVT offset: write address = FLASH_BASE + HAB_OFFSET
FLASH_BASE = 0x60000000
HAB_OFFSET = 0x1000 # IVT offset: write address = FLASH_BASE + HAB_OFFSET
def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
@ -79,9 +81,9 @@ def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
def step(msg: str) -> None:
print(f"\n{''*60}")
print(f"\n{'' * 60}")
print(f" {msg}")
print(f"{''*60}")
print(f"{'' * 60}")
def wait_for_flashloader(timeout: int = 10) -> bool:
@ -91,10 +93,10 @@ def wait_for_flashloader(timeout: int = 10) -> bool:
time.sleep(1)
result = subprocess.run(
["blhost", "-u", BLHOST_USB, "-j", "--", "get-property", "1", "0"],
capture_output=True
capture_output=True,
)
if result.returncode == 0:
print(f" OK ({i+1}с)")
print(f" OK ({i + 1}с)")
return True
print(".", end="", flush=True)
print(" TIMEOUT")
@ -105,7 +107,7 @@ def load_flashloader() -> None:
"""Загружает Flashloader через SDP если ещё не запущен"""
result = subprocess.run(
["blhost", "-u", BLHOST_USB, "-j", "--", "get-property", "1", "0"],
capture_output=True
capture_output=True,
)
if result.returncode == 0:
print(" Flashloader уже запущен — пропускаем загрузку")
@ -118,10 +120,19 @@ def load_flashloader() -> None:
sys.exit(1)
step("Загрузка Flashloader через SDP (1FC9:0130)")
run(["sdphost", "-u", SDP_USB, "-j", "--",
"write-file", "0x20001C00", str(FLASHLOADER)])
run(["sdphost", "-u", SDP_USB, "-j", "--",
"jump-address", "0x20001C00"])
run(
[
"sdphost",
"-u",
SDP_USB,
"-j",
"--",
"write-file",
"0x20001C00",
str(FLASHLOADER),
]
)
run(["sdphost", "-u", SDP_USB, "-j", "--", "jump-address", "0x20001C00"])
if not wait_for_flashloader():
print("[ERROR] Flashloader не ответил. Проверь BOOT_MOD пины и подключение.")
@ -131,10 +142,30 @@ def load_flashloader() -> None:
def configure_flexspi() -> None:
"""Инициализирует FlexSPI NOR контроллер через Flashloader"""
step("Конфигурация FlexSPI NOR (инициализация контроллера)")
run(["blhost", "-u", BLHOST_USB, "--",
"fill-memory", FLEXSPI_OPTION_ADDR, "4", FLEXSPI_OPTION_VALUE, "word"])
run(["blhost", "-u", BLHOST_USB, "--",
"configure-memory", FLEXSPI_MEMORY_ID, FLEXSPI_OPTION_ADDR])
run(
[
"blhost",
"-u",
BLHOST_USB,
"--",
"fill-memory",
FLEXSPI_OPTION_ADDR,
"4",
FLEXSPI_OPTION_VALUE,
"word",
]
)
run(
[
"blhost",
"-u",
BLHOST_USB,
"--",
"configure-memory",
FLEXSPI_MEMORY_ID,
FLEXSPI_OPTION_ADDR,
]
)
def write_fcb() -> None:
@ -145,10 +176,30 @@ def write_fcb() -> None:
Option word 0xF000000F: tag=0xF Write FCB command.
"""
step("Запись FCB в Flash[0x60000000]")
run(["blhost", "-u", BLHOST_USB, "--",
"fill-memory", FLEXSPI_OPTION_ADDR, "4", FLEXSPI_FCB_VALUE, "word"])
run(["blhost", "-u", BLHOST_USB, "--",
"configure-memory", FLEXSPI_MEMORY_ID, FLEXSPI_OPTION_ADDR])
run(
[
"blhost",
"-u",
BLHOST_USB,
"--",
"fill-memory",
FLEXSPI_OPTION_ADDR,
"4",
FLEXSPI_FCB_VALUE,
"word",
]
)
run(
[
"blhost",
"-u",
BLHOST_USB,
"--",
"configure-memory",
FLEXSPI_MEMORY_ID,
FLEXSPI_OPTION_ADDR,
]
)
def flash(hab_bin: Path, ram_only: bool = False) -> None:
@ -159,10 +210,29 @@ def flash(hab_bin: Path, ram_only: bool = False) -> None:
if ram_only:
step(f"Загрузка в RAM (без записи во Flash): {hab_bin.name}")
run(["sdphost", "-u", SDP_USB, "-j", "--",
"write-file", f"0x{FLASH_BASE + HAB_OFFSET:08X}", str(hab_bin)])
run(["sdphost", "-u", SDP_USB, "-j", "--",
"jump-address", f"0x{FLASH_BASE + HAB_OFFSET:08X}"])
run(
[
"sdphost",
"-u",
SDP_USB,
"-j",
"--",
"write-file",
f"0x{FLASH_BASE + HAB_OFFSET:08X}",
str(hab_bin),
]
)
run(
[
"sdphost",
"-u",
SDP_USB,
"-j",
"--",
"jump-address",
f"0x{FLASH_BASE + HAB_OFFSET:08X}",
]
)
return
load_flashloader()
@ -177,14 +247,33 @@ def flash(hab_bin: Path, ram_only: bool = False) -> None:
print(f" Адрес: {write_addr}")
print(f" Стирание: 0x{FLASH_BASE:08X} .. +{erase_size} байт")
run(["blhost", "-u", BLHOST_USB, "--",
"flash-erase-region", f"0x{FLASH_BASE:08X}",
str(erase_size), "0"])
run(
[
"blhost",
"-u",
BLHOST_USB,
"--",
"flash-erase-region",
f"0x{FLASH_BASE:08X}",
str(erase_size),
"0",
]
)
write_fcb()
run(["blhost", "-u", BLHOST_USB, "--",
"write-memory", write_addr, str(hab_bin), "0"])
run(
[
"blhost",
"-u",
BLHOST_USB,
"--",
"write-memory",
write_addr,
str(hab_bin),
"0",
]
)
step("Reset")
run(["blhost", "-u", BLHOST_USB, "--", "reset"])
@ -196,28 +285,32 @@ def main() -> None:
print(f"DEBUG BLHOST_USB = {repr(BLHOST_USB)}")
print(f"DEBUG BUILD_DIR = {repr(BUILD_DIR)}")
parser = argparse.ArgumentParser(description="Прошивка MIMXRT1052 через USB")
parser.add_argument("--firmware", required=True,
choices=["firmware_test", "bootloader", "app"],
help="Имя прошивки")
parser.add_argument("--build-type", required=True,
choices=["Debug", "Release"],
help="Тип сборки")
parser.add_argument("--ram-only", action="store_true",
help="Загрузить в RAM без записи во Flash")
parser.add_argument(
"--firmware",
required=True,
choices=["firmware_test", "bootloader", "app"],
help="Имя прошивки",
)
parser.add_argument(
"--build-type", required=True, choices=["Debug", "Release"], help="Тип сборки"
)
parser.add_argument(
"--ram-only", action="store_true", help="Загрузить в RAM без записи во Flash"
)
args = parser.parse_args()
hab_bin = BUILD_DIR / args.build_type / f"{args.firmware}_hab.bin"
print(f"\n{''*60}")
print(f"\n{'' * 60}")
print(f" MIMXRT1052 Flash Tool")
print(f" Прошивка: {args.firmware} [{args.build_type}]")
print(f" Образ: {hab_bin}")
print(f" SDP USB: {SDP_USB}")
print(f" BL USB: {BLHOST_USB}")
print(f"{''*60}")
print(f"{'' * 60}")
flash(hab_bin, ram_only=args.ram_only)
if __name__ == "__main__":
main()
main()