# fixes: pyformat + O1 release opt level
This commit is contained in:
parent
9be4a2aecc
commit
bbd02f840e
15 changed files with 322 additions and 177 deletions
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"[python]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
}
|
||||
}
|
||||
|
|
@ -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 — подавить
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in a new issue