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

View file

@ -47,6 +47,7 @@ RELAY_OFF_S = 0.1
# Вспомогательные функции # Вспомогательные функции
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def opto_read(ser, ch: int) -> str: def opto_read(ser, ch: int) -> str:
"""Прочитать состояние канала (1-based). Возвращает 'ACTIVE' или 'INACTIVE'.""" """Прочитать состояние канала (1-based). Возвращает 'ACTIVE' или 'INACTIVE'."""
return uart_cmd(ser, f"OPTO_READ {ch}") 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: class TestOptoConnectivity:
"""Базовая проверка: таргет и M5 отвечают.""" """Базовая проверка: таргет и M5 отвечают."""
@ -108,6 +110,7 @@ class TestOptoConnectivity:
# Состояние по умолчанию (все реле выключены) # Состояние по умолчанию (все реле выключены)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestOptoReadDefault: class TestOptoReadDefault:
"""Все каналы INACTIVE без внешнего воздействия.""" """Все каналы INACTIVE без внешнего воздействия."""
@ -132,6 +135,7 @@ class TestOptoReadDefault:
# Активация / деактивация каналов # Активация / деактивация каналов
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestOptoActivateDeactivate: class TestOptoActivateDeactivate:
"""Активация/деактивация каждого канала по отдельности через M5.""" """Активация/деактивация каждого канала по отдельности через M5."""
@ -164,14 +168,16 @@ class TestOptoActivateDeactivate:
self.m5.opto_set(ch, True) self.m5.opto_set(ch, True)
time.sleep(RELAY_ON_S) time.sleep(RELAY_ON_S)
for other in others: for other in others:
assert opto_read(self.ser, other) == "INACTIVE", \ assert opto_read(self.ser, other) == "INACTIVE", (
f"ch{other} должен быть INACTIVE когда активен только ch{ch}" f"ch{other} должен быть INACTIVE когда активен только ch{ch}"
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Callback-события (проверяет ISR + debounce + bsp_opto_process) # Callback-события (проверяет ISR + debounce + bsp_opto_process)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestOptoEvents: class TestOptoEvents:
""" """
Проверяет что прерывание + bsp_opto_process() + коллбэк корректно Проверяет что прерывание + bsp_opto_process() + коллбэк корректно

View file

@ -51,6 +51,7 @@ CAN_RECV_EMPTY_MS = 100
# Вспомогательные функции # Вспомогательные функции
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def can_send_m5(m5, can_id: int, data: list[int], ext: bool = False) -> None: def can_send_m5(m5, can_id: int, data: list[int], ext: bool = False) -> None:
"""M5 отправляет CAN-фрейм на шину.""" """M5 отправляет CAN-фрейм на шину."""
m5.can_send(can_id, data, ext=ext) m5.can_send(can_id, data, ext=ext)
@ -93,7 +94,7 @@ def can_recv_target(ser, timeout_ms: int = CAN_RECV_TIMEOUT_MS) -> dict | None:
can_id = int(parts[0]) can_id = int(parts[0])
ext = bool(int(parts[1])) ext = bool(int(parts[1]))
dlc = int(parts[2]) dlc = int(parts[2])
data = [int(b) for b in parts[3:3 + dlc]] data = [int(b) for b in parts[3 : 3 + dlc]]
return {"id": can_id, "ext": ext, "dlc": dlc, "data": data} return {"id": can_id, "ext": ext, "dlc": dlc, "data": data}
@ -112,6 +113,7 @@ def reset_events(ser) -> None:
# Проверка каналов связи # Проверка каналов связи
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestCanConnectivity: class TestCanConnectivity:
"""Базовая проверка: таргет и M5 отвечают.""" """Базовая проверка: таргет и M5 отвечают."""
@ -131,13 +133,16 @@ class TestCanConnectivity:
def test_m5_can_available(self): def test_m5_can_available(self):
"""M5 сообщает что CAN инициализирован (can_ok=true в info).""" """M5 сообщает что CAN инициализирован (can_ok=true в info)."""
info = self.m5.info() info = self.m5.info()
assert info.get("can_ok"), "M5 CAN не инициализирован — проверьте трансивер и agent.py" assert info.get("can_ok"), (
"M5 CAN не инициализирован — проверьте трансивер и agent.py"
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# M5 → таргет # M5 → таргет
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestCanM5ToTarget: class TestCanM5ToTarget:
"""M5 отправляет фрейм — таргет принимает.""" """M5 отправляет фрейм — таргет принимает."""
@ -200,6 +205,7 @@ class TestCanM5ToTarget:
# Таргет → M5 # Таргет → M5
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestCanTargetToM5: class TestCanTargetToM5:
"""Таргет отправляет фрейм — M5 принимает.""" """Таргет отправляет фрейм — M5 принимает."""
@ -249,14 +255,16 @@ class TestCanTargetToM5:
payload = [0x00, 0xFF, 0x55, 0xAA, 0x0F, 0xF0, 0x01, 0xFE] payload = [0x00, 0xFF, 0x55, 0xAA, 0x0F, 0xF0, 0x01, 0xFE]
assert can_send_target(self.ser, 0x300, payload) == "OK" assert can_send_target(self.ser, 0x300, payload) == "OK"
frame = can_recv_m5(self.m5) frame = can_recv_m5(self.m5)
assert frame["data"] == payload, \ assert frame["data"] == payload, (
f"Данные искажены: ожидали {payload}, получили {frame['data']}" f"Данные искажены: ожидали {payload}, получили {frame['data']}"
)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Фильтрация # Фильтрация
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestCanFiltering: class TestCanFiltering:
""" """
Проверка фильтрации по ID: только нужные фреймы проходят, Проверка фильтрации по ID: только нужные фреймы проходят,
@ -289,7 +297,9 @@ class TestCanFiltering:
can_send_m5(self.m5, target_id, [0xAA]) can_send_m5(self.m5, target_id, [0xAA])
frame = can_recv_target(self.ser) frame = can_recv_target(self.ser)
assert frame is not None, "Фрейм с нужным ID не принят" assert frame is not None, "Фрейм с нужным ID не принят"
assert frame["id"] == target_id, f"Получен ID {frame['id']:#x}, ожидали {target_id:#x}" assert frame["id"] == target_id, (
f"Получен ID {frame['id']:#x}, ожидали {target_id:#x}"
)
def test_filter_wrong_id_blocked(self): def test_filter_wrong_id_blocked(self):
"""set_filter с точной маской блокирует другой ID.""" """set_filter с точной маской блокирует другой ID."""
@ -299,8 +309,9 @@ class TestCanFiltering:
can_send_m5(self.m5, wrong_id, [0xBB]) can_send_m5(self.m5, wrong_id, [0xBB])
frame = can_recv_target(self.ser, timeout_ms=CAN_RECV_EMPTY_MS) 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} прошёл фильтр, хотя не должен был" f"Фрейм с ID {wrong_id:#x} прошёл фильтр, хотя не должен был"
)
def test_filter_mask_passes_group(self): def test_filter_mask_passes_group(self):
"""Маска 0x7F0 пропускает группу ID с одинаковыми старшими битами.""" """Маска 0x7F0 пропускает группу ID с одинаковыми старшими битами."""
@ -353,6 +364,7 @@ class TestCanFiltering:
# STD и EXT раздельно # STD и EXT раздельно
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestCanFrameTypes: class TestCanFrameTypes:
"""STD и EXT фреймы не перепутываются — is_extended корректен.""" """STD и EXT фреймы не перепутываются — is_extended корректен."""

View file

@ -25,9 +25,9 @@ def _operator_prompt(msg: str) -> None:
input(f"\n >>> {msg}\n Нажмите Enter когда готово...") input(f"\n >>> {msg}\n Нажмите Enter когда готово...")
time.sleep(DEBOUNCE_SETTLE_S) time.sleep(DEBOUNCE_SETTLE_S)
@pytest.mark.interactive @pytest.mark.interactive
class TestHilButton: class TestHilButton:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def _setup(self, uart_button): def _setup(self, uart_button):
self.ser = uart_button self.ser = uart_button

View file

@ -43,6 +43,7 @@ def usb_cdc_echo(ser, data: bytes, timeout_s: float = USB_CDC_READ_TIMEOUT_S) ->
return result return result
@pytest.mark.usb_vcom @pytest.mark.usb_vcom
class TestUsbCdcBasic: class TestUsbCdcBasic:
"""Базовые тесты USB CDC ACM — enumeration, echo, payload sizes.""" """Базовые тесты USB CDC ACM — enumeration, echo, payload sizes."""

View file

@ -28,6 +28,7 @@ CAN_TX_DATA = [0xCA, 0xFE, 0xBA, 0xBE]
# Таймаут для M5 can_recv — с запасом относительно CAN_RX_TIMEOUT_MS (500 мс) в прошивке # Таймаут для M5 can_recv — с запасом относительно CAN_RX_TIMEOUT_MS (500 мс) в прошивке
CAN_RECV_TIMEOUT_MS = 1000 CAN_RECV_TIMEOUT_MS = 1000
@pytest.mark.usb_vcom @pytest.mark.usb_vcom
class TestFirmwareCan: class TestFirmwareCan:
"""Тест CAN-интерфейса через firmware_test CDC (протокол v2).""" """Тест CAN-интерфейса через firmware_test CDC (протокол v2)."""
@ -37,7 +38,6 @@ class TestFirmwareCan:
self.cdc = firmware_cdc self.cdc = firmware_cdc
self.m5 = m5 self.m5 = m5
def _on_confirm(self, confirm_id: str) -> bool: def _on_confirm(self, confirm_id: str) -> bool:
""" """
Оркестратор CAN-теста. Оркестратор CAN-теста.
@ -61,12 +61,15 @@ class TestFirmwareCan:
try: try:
frame = self.m5.can_recv(timeout_ms=CAN_RECV_TIMEOUT_MS) frame = self.m5.can_recv(timeout_ms=CAN_RECV_TIMEOUT_MS)
import logging import logging
logging.getLogger(__name__).info( logging.getLogger(__name__).info(
"M5 received frame: id=0x%X data=%s", "M5 received frame: id=0x%X data=%s",
frame.get("id", -1), frame.get("data", []) frame.get("id", -1),
frame.get("data", []),
) )
except (TimeoutError, RuntimeError) as e: except (TimeoutError, RuntimeError) as e:
import logging import logging
logging.getLogger(__name__).warning("M5 can_recv failed: %s", e) logging.getLogger(__name__).warning("M5 can_recv failed: %s", e)
return False return False
@ -91,7 +94,6 @@ class TestFirmwareCan:
"""Базовая проверка CDC-канала.""" """Базовая проверка CDC-канала."""
self.cdc.ping() self.cdc.ping()
def test_can_pass(self) -> None: def test_can_pass(self) -> None:
""" """
Полный прогон test_can: RX + TX через M5. Полный прогон test_can: RX + TX через M5.
@ -104,8 +106,7 @@ class TestFirmwareCan:
) )
assert result.get("status") == "pass", ( assert result.get("status") == "pass", (
f"test_can вернул {result.get('status')!r}: " f"test_can вернул {result.get('status')!r}: {result.get('detail', '')}"
f"{result.get('detail', '')}"
) )
def test_can_rx_fail_no_frame(self) -> None: def test_can_rx_fail_no_frame(self) -> None:
@ -131,6 +132,4 @@ class TestFirmwareCan:
f"Ожидали fail, получили {result.get('status')!r}" f"Ожидали fail, получили {result.get('status')!r}"
) )
detail = result.get("detail", "") detail = result.get("detail", "")
assert "no frame" in detail, ( assert "no frame" in detail, f"detail должен содержать 'no frame': {detail!r}"
f"detail должен содержать 'no frame': {detail!r}"
)

View file

@ -39,6 +39,7 @@ _OPTO_ACTIONS: dict[str, tuple[int, bool]] = {
"opto_rs_inactive": (3, False), "opto_rs_inactive": (3, False),
} }
@pytest.mark.usb_vcom @pytest.mark.usb_vcom
class TestFirmwareOpto: class TestFirmwareOpto:
"""Тест оптовходов через firmware_test CDC (протокол v2).""" """Тест оптовходов через firmware_test CDC (протокол v2)."""
@ -86,8 +87,7 @@ class TestFirmwareOpto:
) )
assert result.get("status") == "pass", ( assert result.get("status") == "pass", (
f"test_opto вернул {result.get('status')!r}: " f"test_opto вернул {result.get('status')!r}: {result.get('detail', '')}"
f"{result.get('detail', '')}"
) )
def test_opto_in1_fail_on_inactive(self) -> None: def test_opto_in1_fail_on_inactive(self) -> None:
@ -111,6 +111,4 @@ class TestFirmwareOpto:
f"Ожидали fail, получили {result.get('status')!r}" f"Ожидали fail, получили {result.get('status')!r}"
) )
detail = result.get("detail", "") detail = result.get("detail", "")
assert "mismatch" in detail, ( assert "mismatch" in detail, f"detail должен содержать 'mismatch': {detail!r}"
f"detail должен содержать 'mismatch': {detail!r}"
)

View file

@ -105,9 +105,7 @@ class M5Agent:
except json.JSONDecodeError: except json.JSONDecodeError:
continue continue
if not resp.get("ok"): if not resp.get("ok"):
raise RuntimeError( raise RuntimeError(f"M5 error: {resp.get('err', '?')} (cmd={command})")
f"M5 error: {resp.get('err', '?')} (cmd={command})"
)
return resp return resp
raise TimeoutError(f"M5: нет ответа на '{command}'") raise TimeoutError(f"M5: нет ответа на '{command}'")
@ -124,6 +122,7 @@ class M5Agent:
def opto_all_off(self) -> None: def opto_all_off(self) -> None:
self.cmd("opto_all_off") self.cmd("opto_all_off")
def info(self) -> dict: def info(self) -> dict:
"""Запросить информацию об агенте (включая can_ok).""" """Запросить информацию об агенте (включая can_ok)."""
return self.cmd("info") return self.cmd("info")
@ -205,6 +204,7 @@ def m5(request: pytest.FixtureRequest) -> Generator[M5Agent, None, None]:
# Фикстуры загрузки (scope=module — один раз на файл с тестами) # Фикстуры загрузки (scope=module — один раз на файл с тестами)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def loaded_host_uart(request: pytest.FixtureRequest, m5: M5Agent) -> None: def loaded_host_uart(request: pytest.FixtureRequest, m5: M5Agent) -> None:
""" """
@ -219,6 +219,7 @@ def loaded_host_uart(request: pytest.FixtureRequest, m5: M5Agent) -> None:
Path(cfg.BUILD_DIR) / "tests/target/host_uart/test_host_uart.elf", Path(cfg.BUILD_DIR) / "tests/target/host_uart/test_host_uart.elf",
) )
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def loaded_hil_button(request: pytest.FixtureRequest, m5: M5Agent) -> None: def loaded_hil_button(request: pytest.FixtureRequest, m5: M5Agent) -> None:
""" """
@ -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", Path(cfg.BUILD_DIR) / "tests/target/hil_button/test_hil_button.elf",
) )
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None: def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None:
""" """
@ -249,6 +251,7 @@ def loaded_hil_opto(request: pytest.FixtureRequest, m5: M5Agent) -> None:
Path(cfg.BUILD_DIR) / "tests/target/hil_opto/test_hil_opto.elf", Path(cfg.BUILD_DIR) / "tests/target/hil_opto/test_hil_opto.elf",
) )
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def loaded_hil_can(request: pytest.FixtureRequest, m5: M5Agent) -> None: def loaded_hil_can(request: pytest.FixtureRequest, m5: M5Agent) -> None:
""" """
@ -263,6 +266,7 @@ 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") @pytest.fixture(scope="module")
def loaded_hil_usb_cdc(request: pytest.FixtureRequest, m5: M5Agent) -> None: def loaded_hil_usb_cdc(request: pytest.FixtureRequest, m5: M5Agent) -> None:
""" """
@ -277,6 +281,7 @@ 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", Path(cfg.BUILD_DIR) / "tests/target/hil_usb_cdc/test_hil_usb_cdc.elf",
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Фикстуры UART (установление соединения с VCOM программатора NXP MCU-Link) # Фикстуры UART (установление соединения с VCOM программатора NXP MCU-Link)
# Выполняется один раз на тест # Выполняется один раз на тест
@ -288,18 +293,26 @@ _UART_FIXTURE_MAP = {
"uart_button": "loaded_hil_button", "uart_button": "loaded_hil_button",
"uart_hil_usb_cdc": "loaded_hil_usb_cdc", "uart_hil_usb_cdc": "loaded_hil_usb_cdc",
} }
def _make_uart_fixture(loaded_name: str): def _make_uart_fixture(loaded_name: str):
@pytest.fixture(scope="module") @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) # триггерит зависимость явно request.getfixturevalue(loaded_name) # триггерит зависимость явно
port = request.config.getoption("--vcom") port = request.config.getoption("--vcom")
with _uart_context(port, cfg.VCOM_BAUD, cfg.READY_TIMEOUT) as ser: with _uart_context(port, cfg.VCOM_BAUD, cfg.READY_TIMEOUT) as ser:
yield ser yield ser
return _fixture return _fixture
# Регистрируем все фикстуры в пространстве имён модуля одной строкой # Регистрируем все фикстуры в пространстве имён модуля одной строкой
for _name, _dep in _UART_FIXTURE_MAP.items(): for _name, _dep in _UART_FIXTURE_MAP.items():
globals()[_name] = _make_uart_fixture(_dep) globals()[_name] = _make_uart_fixture(_dep)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Фикстуры USB CDC (открытие порта, установка DTR) # Фикстуры USB CDC (открытие порта, установка DTR)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -318,13 +331,17 @@ def usb_cdc_port(
ser = None ser = None
while time.monotonic() < deadline: while time.monotonic() < deadline:
try: 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 break
except serial.SerialException: except serial.SerialException:
time.sleep(0.3) time.sleep(0.3)
if ser is None: 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: try:
# Установить DTR чтобы firmware увидела DTE presence. # Установить DTR чтобы firmware увидела DTE presence.
@ -354,6 +371,7 @@ def uart_cmd(ser: serial.Serial, cmd: str) -> str:
# firmware_test CDC — фикстура для тестирования firmware_test через протокол v2 # firmware_test CDC — фикстура для тестирования firmware_test через протокол v2
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class FirmwareCdc: class FirmwareCdc:
""" """
Драйвер firmware_test протокола v2 (JSON-lines через USB CDC). Драйвер firmware_test протокола v2 (JSON-lines через USB CDC).
@ -395,7 +413,9 @@ class FirmwareCdc:
msg = self._readline(timeout_s=max(remaining, 0.1)) msg = self._readline(timeout_s=max(remaining, 0.1))
if msg.get("type") == event_type: if msg.get("type") == event_type:
return msg 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( raise TimeoutError(
f"firmware_cdc: событие {event_type!r} не получено за {timeout_s} с" 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: if ser is None:
pytest.fail( pytest.fail(
f"CDC порт {cfg.TARGET_VCOM_PORT} недоступен " f"CDC порт {cfg.TARGET_VCOM_PORT} недоступен за {cfg.TARGET_VCOM_TIMEOUT} с"
f"за {cfg.TARGET_VCOM_TIMEOUT} с"
) )
try: try:

View file

@ -14,12 +14,12 @@ from pathlib import Path
_REPO_ROOT = Path(__file__).resolve().parents[2] _REPO_ROOT = Path(__file__).resolve().parents[2]
def _get(key: str, default: str) -> str: def _get(key: str, default: str) -> str:
return os.environ.get(key, default) return os.environ.get(key, default)
BUILD_DIR: str = _get("HIL_BUILD_DIR", BUILD_DIR: str = _get("HIL_BUILD_DIR", str(_REPO_ROOT / "build" / "target-debug"))
str(_REPO_ROOT / "build" / "target-debug"))
PYOCD_FREQUENCY: int = int(_get("HIL_PYOCD_FREQUENCY", "1000000")) PYOCD_FREQUENCY: int = int(_get("HIL_PYOCD_FREQUENCY", "1000000"))
@ -36,6 +36,6 @@ M5_BAUD: int = int(_get("HIL_M5_BAUD", "115200"))
M5_TIMEOUT: float = float(_get("HIL_M5_TIMEOUT", "3.0")) M5_TIMEOUT: float = float(_get("HIL_M5_TIMEOUT", "3.0"))
# VCOM на плате таргета # VCOM на плате таргета
TARGET_VCOM_PORT:str = _get("HIL_USB_CDC_PORT", "/dev/ttyACM2") TARGET_VCOM_PORT: str = _get("HIL_USB_CDC_PORT", "/dev/ttyACM2")
TARGET_VCOM_BAUD:int = int(_get("HIL_USB_CDC_BAUD", "115200")) 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_TIMEOUT: float = float(_get("HIL_USB_CDC_TIMEOUT", "3.0"))

View file

@ -48,9 +48,11 @@ REPO_ROOT = Path(__file__).resolve().parents[2]
# ── Конфигурация из окружения (just экспортирует .env через set export) ─────── # ── Конфигурация из окружения (just экспортирует .env через set export) ───────
def _env(key: str, default: str) -> str: def _env(key: str, default: str) -> str:
return os.environ.get(key, default) 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") PYOCD_FREQUENCY = _env("PYOCD_FREQUENCY", "4000000")
BUILD_DIR = Path(_env("BUILD_DIR", str(REPO_ROOT / "build"))) BUILD_DIR = Path(_env("BUILD_DIR", str(REPO_ROOT / "build")))
@ -71,6 +73,7 @@ HAB_NAMES = {
# ── Helpers ─────────────────────────────────────────────────────────────────── # ── Helpers ───────────────────────────────────────────────────────────────────
def build_full_image(fcb_path: Path, hab_path: Path) -> bytes: def build_full_image(fcb_path: Path, hab_path: Path) -> bytes:
"""Объединить FCB + padding + HAB в единый образ для записи с 0x60000000.""" """Объединить FCB + padding + HAB в единый образ для записи с 0x60000000."""
fcb = fcb_path.read_bytes() fcb = fcb_path.read_bytes()
@ -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: def run_pyocd_flash(image_path: Path, target: str, frequency: str) -> int:
"""Запустить pyocd flash через uv run из tools/hil.""" """Запустить pyocd flash через uv run из tools/hil."""
cmd = [ cmd = [
"uv", "run", "uv",
"--directory", str(HIL_DIR), "run",
"pyocd", "flash", "--directory",
"--target", target, str(HIL_DIR),
"--frequency", frequency, "pyocd",
"--base-address", "0x60000000", "flash",
"--erase", "sector", "--target",
target,
"--frequency",
frequency,
"--base-address",
"0x60000000",
"--erase",
"sector",
str(image_path), str(image_path),
] ]
print(f"\n Running: {' '.join(cmd)}\n") print(f"\n Running: {' '.join(cmd)}\n")
@ -110,6 +120,7 @@ def run_pyocd_flash(image_path: Path, target: str, frequency: str) -> int:
# ── Main ────────────────────────────────────────────────────────────────────── # ── Main ──────────────────────────────────────────────────────────────────────
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Flash MIMXRT1052 via SWD (MCU-Link). " description="Flash MIMXRT1052 via SWD (MCU-Link). "

View file

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