# 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 "")
|
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 — подавить
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ from conftest import uart_cmd
|
||||||
# process() цикл ~10 мс (CLI_RX_TIMEOUT)
|
# process() цикл ~10 мс (CLI_RX_TIMEOUT)
|
||||||
# UART round-trip ~2 мс
|
# UART round-trip ~2 мс
|
||||||
# Итого минимум ~33 мс. Берём с запасом × 3.
|
# Итого минимум ~33 мс. Берём с запасом × 3.
|
||||||
RELAY_ON_S = 0.1 # ждать после включения реле
|
RELAY_ON_S = 0.1 # ждать после включения реле
|
||||||
|
|
||||||
# После выключения нужно убедиться что INACTIVE подтверждён дебаунсом.
|
# После выключения нужно убедиться что INACTIVE подтверждён дебаунсом.
|
||||||
# Берём чуть больше чем RELAY_ON_S — дебаунс работает симметрично.
|
# Берём чуть больше чем RELAY_ON_S — дебаунс работает симметрично.
|
||||||
|
|
@ -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() + коллбэк корректно
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ from conftest import uart_cmd
|
||||||
|
|
||||||
# При 125 kbit/s один фрейм занимает ~0.1 мс.
|
# При 125 kbit/s один фрейм занимает ~0.1 мс.
|
||||||
# Добавляем запас на задержку USB CDC (M5 ↔ хост) и polling в прошивке.
|
# Добавляем запас на задержку USB CDC (M5 ↔ хост) и polling в прошивке.
|
||||||
CAN_SETTLE_S = 0.05 # ждать после отправки перед чтением
|
CAN_SETTLE_S = 0.05 # ждать после отправки перед чтением
|
||||||
|
|
||||||
# Таймаут CAN_RECV на таргете (мс) — передаётся в команду.
|
# Таймаут CAN_RECV на таргете (мс) — передаётся в команду.
|
||||||
# Должен быть достаточным для round-trip через шину + USB CDC M5.
|
# Должен быть достаточным для 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:
|
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)
|
||||||
|
|
@ -70,10 +71,10 @@ def can_send_target(ser, can_id: int, data: list[int], ext: bool = False) -> str
|
||||||
Таргет отправляет CAN-фрейм.
|
Таргет отправляет CAN-фрейм.
|
||||||
Возвращает ответ прошивки: 'OK', 'ERR_TIMEOUT', 'ERR_BUSY', 'ERR_PARAM'.
|
Возвращает ответ прошивки: 'OK', 'ERR_TIMEOUT', 'ERR_BUSY', 'ERR_PARAM'.
|
||||||
"""
|
"""
|
||||||
dlc = len(data)
|
dlc = len(data)
|
||||||
data_str = " ".join(str(b) for b in data)
|
data_str = " ".join(str(b) for b in data)
|
||||||
ext_flag = 1 if ext else 0
|
ext_flag = 1 if ext else 0
|
||||||
cmd = f"CAN_SEND {can_id} {ext_flag} {dlc}"
|
cmd = f"CAN_SEND {can_id} {ext_flag} {dlc}"
|
||||||
if dlc > 0:
|
if dlc > 0:
|
||||||
cmd += f" {data_str}"
|
cmd += f" {data_str}"
|
||||||
return uart_cmd(ser, cmd)
|
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:
|
if len(parts) < 3:
|
||||||
raise ValueError(f"Неожиданный ответ CAN_RECV: {resp!r}")
|
raise ValueError(f"Неожиданный ответ CAN_RECV: {resp!r}")
|
||||||
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,13 +113,14 @@ def reset_events(ser) -> None:
|
||||||
# Проверка каналов связи
|
# Проверка каналов связи
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestCanConnectivity:
|
class TestCanConnectivity:
|
||||||
"""Базовая проверка: таргет и M5 отвечают."""
|
"""Базовая проверка: таргет и M5 отвечают."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _setup(self, uart_can, m5):
|
def _setup(self, uart_can, m5):
|
||||||
self.ser = uart_can
|
self.ser = uart_can
|
||||||
self.m5 = m5
|
self.m5 = m5
|
||||||
|
|
||||||
def test_target_ping(self):
|
def test_target_ping(self):
|
||||||
"""PING → PONG: UART-канал host↔target работает."""
|
"""PING → PONG: UART-канал host↔target работает."""
|
||||||
|
|
@ -131,20 +133,23 @@ 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 отправляет фрейм — таргет принимает."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _setup(self, uart_can, m5):
|
def _setup(self, uart_can, m5):
|
||||||
self.ser = uart_can
|
self.ser = uart_can
|
||||||
self.m5 = m5
|
self.m5 = m5
|
||||||
uart_cmd(self.ser, "CAN_ACCEPT_ALL")
|
uart_cmd(self.ser, "CAN_ACCEPT_ALL")
|
||||||
reset_events(self.ser)
|
reset_events(self.ser)
|
||||||
|
|
||||||
|
|
@ -153,10 +158,10 @@ class TestCanM5ToTarget:
|
||||||
payload = [0x11, 0x22, 0x33]
|
payload = [0x11, 0x22, 0x33]
|
||||||
can_send_m5(self.m5, 0x123, payload)
|
can_send_m5(self.m5, 0x123, payload)
|
||||||
frame = can_recv_target(self.ser)
|
frame = can_recv_target(self.ser)
|
||||||
assert frame is not None, "Таргет не принял фрейм от M5"
|
assert frame is not None, "Таргет не принял фрейм от M5"
|
||||||
assert frame["id"] == 0x123, f"Неверный ID: {frame['id']:#x}"
|
assert frame["id"] == 0x123, f"Неверный ID: {frame['id']:#x}"
|
||||||
assert frame["ext"] is False, "Ожидали STD-фрейм"
|
assert frame["ext"] is False, "Ожидали STD-фрейм"
|
||||||
assert frame["dlc"] == 3, f"Неверный DLC: {frame['dlc']}"
|
assert frame["dlc"] == 3, f"Неверный DLC: {frame['dlc']}"
|
||||||
assert frame["data"] == payload, f"Неверные данные: {frame['data']}"
|
assert frame["data"] == payload, f"Неверные данные: {frame['data']}"
|
||||||
|
|
||||||
def test_ext_frame_received(self):
|
def test_ext_frame_received(self):
|
||||||
|
|
@ -164,10 +169,10 @@ class TestCanM5ToTarget:
|
||||||
payload = [0xAA, 0xBB]
|
payload = [0xAA, 0xBB]
|
||||||
can_send_m5(self.m5, 0x1ABCDEF, payload, ext=True)
|
can_send_m5(self.m5, 0x1ABCDEF, payload, ext=True)
|
||||||
frame = can_recv_target(self.ser)
|
frame = can_recv_target(self.ser)
|
||||||
assert frame is not None, "Таргет не принял EXT-фрейм"
|
assert frame is not None, "Таргет не принял EXT-фрейм"
|
||||||
assert frame["id"] == 0x1ABCDEF, f"Неверный EXT ID: {frame['id']:#x}"
|
assert frame["id"] == 0x1ABCDEF, f"Неверный EXT ID: {frame['id']:#x}"
|
||||||
assert frame["ext"] is True, "Ожидали EXT-фрейм"
|
assert frame["ext"] is True, "Ожидали EXT-фрейм"
|
||||||
assert frame["data"] == payload, f"Неверные данные: {frame['data']}"
|
assert frame["data"] == payload, f"Неверные данные: {frame['data']}"
|
||||||
|
|
||||||
def test_max_dlc_frame(self):
|
def test_max_dlc_frame(self):
|
||||||
"""M5 TX 8-байтовый фрейм — таргет принимает все 8 байт корректно."""
|
"""M5 TX 8-байтовый фрейм — таргет принимает все 8 байт корректно."""
|
||||||
|
|
@ -175,7 +180,7 @@ class TestCanM5ToTarget:
|
||||||
can_send_m5(self.m5, 0x7FF, payload)
|
can_send_m5(self.m5, 0x7FF, payload)
|
||||||
frame = can_recv_target(self.ser)
|
frame = can_recv_target(self.ser)
|
||||||
assert frame is not None
|
assert frame is not None
|
||||||
assert frame["dlc"] == 8
|
assert frame["dlc"] == 8
|
||||||
assert frame["data"] == payload
|
assert frame["data"] == payload
|
||||||
|
|
||||||
def test_zero_dlc_frame(self):
|
def test_zero_dlc_frame(self):
|
||||||
|
|
@ -183,7 +188,7 @@ class TestCanM5ToTarget:
|
||||||
can_send_m5(self.m5, 0x001, [])
|
can_send_m5(self.m5, 0x001, [])
|
||||||
frame = can_recv_target(self.ser)
|
frame = can_recv_target(self.ser)
|
||||||
assert frame is not None
|
assert frame is not None
|
||||||
assert frame["dlc"] == 0
|
assert frame["dlc"] == 0
|
||||||
assert frame["data"] == []
|
assert frame["data"] == []
|
||||||
|
|
||||||
def test_rx_event_counter_increments(self):
|
def test_rx_event_counter_increments(self):
|
||||||
|
|
@ -200,21 +205,22 @@ class TestCanM5ToTarget:
|
||||||
# Таргет → M5
|
# Таргет → M5
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestCanTargetToM5:
|
class TestCanTargetToM5:
|
||||||
"""Таргет отправляет фрейм — M5 принимает."""
|
"""Таргет отправляет фрейм — M5 принимает."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _setup(self, uart_can, m5):
|
def _setup(self, uart_can, m5):
|
||||||
self.ser = uart_can
|
self.ser = uart_can
|
||||||
self.m5 = m5
|
self.m5 = m5
|
||||||
|
|
||||||
def test_std_frame_sent(self):
|
def test_std_frame_sent(self):
|
||||||
"""Таргет TX STD → M5 принимает с верным ID и данными."""
|
"""Таргет TX STD → M5 принимает с верным ID и данными."""
|
||||||
payload = [0xDE, 0xAD, 0xBE, 0xEF]
|
payload = [0xDE, 0xAD, 0xBE, 0xEF]
|
||||||
assert can_send_target(self.ser, 0x456, payload) == "OK"
|
assert can_send_target(self.ser, 0x456, payload) == "OK"
|
||||||
frame = can_recv_m5(self.m5)
|
frame = can_recv_m5(self.m5)
|
||||||
assert frame["id"] == 0x456, f"Неверный ID: {frame['id']:#x}"
|
assert frame["id"] == 0x456, f"Неверный ID: {frame['id']:#x}"
|
||||||
assert frame["ext"] is False, "Ожидали STD-фрейм"
|
assert frame["ext"] is False, "Ожидали STD-фрейм"
|
||||||
assert frame["data"] == payload, f"Неверные данные: {frame['data']}"
|
assert frame["data"] == payload, f"Неверные данные: {frame['data']}"
|
||||||
|
|
||||||
def test_ext_frame_sent(self):
|
def test_ext_frame_sent(self):
|
||||||
|
|
@ -222,8 +228,8 @@ class TestCanTargetToM5:
|
||||||
payload = [0x01, 0x02]
|
payload = [0x01, 0x02]
|
||||||
assert can_send_target(self.ser, 0x1FFFFFF, payload, ext=True) == "OK"
|
assert can_send_target(self.ser, 0x1FFFFFF, payload, ext=True) == "OK"
|
||||||
frame = can_recv_m5(self.m5)
|
frame = can_recv_m5(self.m5)
|
||||||
assert frame["id"] == 0x1FFFFFF, f"Неверный EXT ID: {frame['id']:#x}"
|
assert frame["id"] == 0x1FFFFFF, f"Неверный EXT ID: {frame['id']:#x}"
|
||||||
assert frame["ext"] is True, "Ожидали EXT-фрейм"
|
assert frame["ext"] is True, "Ожидали EXT-фрейм"
|
||||||
|
|
||||||
def test_max_std_id(self):
|
def test_max_std_id(self):
|
||||||
"""Максимальный достижимый STD ID через M5 recv — проверяем 0x7FE."""
|
"""Максимальный достижимый STD ID через M5 recv — проверяем 0x7FE."""
|
||||||
|
|
@ -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: только нужные фреймы проходят,
|
||||||
|
|
@ -266,7 +274,7 @@ class TestCanFiltering:
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _setup(self, uart_can, m5):
|
def _setup(self, uart_can, m5):
|
||||||
self.ser = uart_can
|
self.ser = uart_can
|
||||||
self.m5 = m5
|
self.m5 = m5
|
||||||
reset_events(self.ser)
|
reset_events(self.ser)
|
||||||
|
|
||||||
def test_accept_all_receives_any_id(self):
|
def test_accept_all_receives_any_id(self):
|
||||||
|
|
@ -288,24 +296,27 @@ 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."""
|
||||||
target_id = 0x123
|
target_id = 0x123
|
||||||
wrong_id = 0x456
|
wrong_id = 0x456
|
||||||
assert set_filter_target(self.ser, 0, target_id, 0x7FF) == "OK"
|
assert set_filter_target(self.ser, 0, target_id, 0x7FF) == "OK"
|
||||||
|
|
||||||
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 с одинаковыми старшими битами."""
|
||||||
base_id = 0x120
|
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"
|
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)
|
can_send_m5(self.m5, target_id, [0x42], ext=True)
|
||||||
frame = can_recv_target(self.ser)
|
frame = can_recv_target(self.ser)
|
||||||
assert frame is not None, "EXT-фрейм с нужным ID не принят"
|
assert frame is not None, "EXT-фрейм с нужным ID не принят"
|
||||||
assert frame["id"] == target_id
|
assert frame["id"] == target_id
|
||||||
assert frame["ext"] is True
|
assert frame["ext"] is True
|
||||||
|
|
||||||
def test_accept_all_after_filter(self):
|
def test_accept_all_after_filter(self):
|
||||||
|
|
@ -353,13 +364,14 @@ class TestCanFiltering:
|
||||||
# STD и EXT раздельно
|
# STD и EXT раздельно
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class TestCanFrameTypes:
|
class TestCanFrameTypes:
|
||||||
"""STD и EXT фреймы не перепутываются — is_extended корректен."""
|
"""STD и EXT фреймы не перепутываются — is_extended корректен."""
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def _setup(self, uart_can, m5):
|
def _setup(self, uart_can, m5):
|
||||||
self.ser = uart_can
|
self.ser = uart_can
|
||||||
self.m5 = m5
|
self.m5 = m5
|
||||||
uart_cmd(self.ser, "CAN_ACCEPT_ALL")
|
uart_cmd(self.ser, "CAN_ACCEPT_ALL")
|
||||||
|
|
||||||
def test_std_frame_is_not_ext(self):
|
def test_std_frame_is_not_ext(self):
|
||||||
|
|
@ -374,15 +386,15 @@ class TestCanFrameTypes:
|
||||||
can_send_m5(self.m5, 0x1FF, [0x01], ext=True)
|
can_send_m5(self.m5, 0x1FF, [0x01], ext=True)
|
||||||
frame = can_recv_target(self.ser)
|
frame = can_recv_target(self.ser)
|
||||||
assert frame is not None
|
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):
|
def test_std_id_range_boundary(self):
|
||||||
"""Граничные STD ID: 0x000 и 0x7FF."""
|
"""Граничные STD ID: 0x000 и 0x7FF."""
|
||||||
for can_id in [0x000, 0x7FF]:
|
for can_id in [0x000, 0x7FF]:
|
||||||
can_send_m5(self.m5, can_id, [0xBB])
|
can_send_m5(self.m5, can_id, [0xBB])
|
||||||
frame = can_recv_target(self.ser)
|
frame = can_recv_target(self.ser)
|
||||||
assert frame is not None, f"ID {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["id"] == can_id, f"ID {frame['id']:#x} ≠ {can_id:#x}"
|
||||||
assert frame["ext"] is False
|
assert frame["ext"] is False
|
||||||
|
|
||||||
def test_ext_id_range_boundary(self):
|
def test_ext_id_range_boundary(self):
|
||||||
|
|
@ -390,6 +402,6 @@ class TestCanFrameTypes:
|
||||||
for can_id in [0x000, 0x1FFFFFFF]:
|
for can_id in [0x000, 0x1FFFFFFF]:
|
||||||
can_send_m5(self.m5, can_id, [0xCC], ext=True)
|
can_send_m5(self.m5, can_id, [0xCC], ext=True)
|
||||||
frame = can_recv_target(self.ser)
|
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["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 когда готово...")
|
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
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -20,14 +20,15 @@ import pytest
|
||||||
from conftest import FirmwareCdc, M5Agent
|
from conftest import FirmwareCdc, M5Agent
|
||||||
|
|
||||||
# CAN параметры — должны совпадать с test_can.c
|
# CAN параметры — должны совпадать с test_can.c
|
||||||
CAN_RX_ID = 0x100
|
CAN_RX_ID = 0x100
|
||||||
CAN_RX_DATA = [0xDE, 0xAD, 0xBE, 0xEF]
|
CAN_RX_DATA = [0xDE, 0xAD, 0xBE, 0xEF]
|
||||||
CAN_TX_ID = 0x200
|
CAN_TX_ID = 0x200
|
||||||
CAN_TX_DATA = [0xCA, 0xFE, 0xBA, 0xBE]
|
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}"
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -25,20 +25,21 @@ import pytest
|
||||||
from conftest import FirmwareCdc, M5Agent
|
from conftest import FirmwareCdc, M5Agent
|
||||||
|
|
||||||
|
|
||||||
RELAY_ON_S = 0.1 # реле замыкается быстро
|
RELAY_ON_S = 0.1 # реле замыкается быстро
|
||||||
RELAY_OFF_S = 0.25 # размыкание + дебаунс прошивки с запасом
|
RELAY_OFF_S = 0.25 # размыкание + дебаунс прошивки с запасом
|
||||||
|
|
||||||
|
|
||||||
# Маппинг confirm_id → (opto_ch, state)
|
# Маппинг confirm_id → (opto_ch, state)
|
||||||
_OPTO_ACTIONS: dict[str, tuple[int, bool]] = {
|
_OPTO_ACTIONS: dict[str, tuple[int, bool]] = {
|
||||||
"opto_in1_active": (1, True),
|
"opto_in1_active": (1, True),
|
||||||
"opto_in1_inactive": (1, False),
|
"opto_in1_inactive": (1, False),
|
||||||
"opto_in2_active": (2, True),
|
"opto_in2_active": (2, True),
|
||||||
"opto_in2_inactive": (2, False),
|
"opto_in2_inactive": (2, False),
|
||||||
"opto_rs_active": (3, True),
|
"opto_rs_active": (3, True),
|
||||||
"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}"
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ log = logging.getLogger(__name__)
|
||||||
# CLI-опции pytest (перекрывают .env и os.environ)
|
# CLI-опции pytest (перекрывают .env и os.environ)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||||
parser.addoption("--elf", default=None, help="Путь к .elf файлу")
|
parser.addoption("--elf", default=None, help="Путь к .elf файлу")
|
||||||
parser.addoption("--vcom", default=cfg.VCOM_PORT, help="VCOM-порт MCU-Link")
|
parser.addoption("--vcom", default=cfg.VCOM_PORT, help="VCOM-порт MCU-Link")
|
||||||
parser.addoption("--m5-port", default=None, help="M5StampPLC serial port")
|
parser.addoption("--m5-port", default=None, help="M5StampPLC serial port")
|
||||||
parser.addoption("--no-load", action="store_true", help="ELF уже запущен")
|
parser.addoption("--no-load", action="store_true", help="ELF уже запущен")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
@ -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,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",
|
Path(cfg.BUILD_DIR) / "tests/target/hil_usb_cdc/test_hil_usb_cdc.elf",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Фикстуры UART (установление соединения с VCOM программатора NXP MCU-Link)
|
# Фикстуры UART (установление соединения с VCOM программатора NXP MCU-Link)
|
||||||
# Выполняется один раз на тест
|
# Выполняется один раз на тест
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_UART_FIXTURE_MAP = {
|
_UART_FIXTURE_MAP = {
|
||||||
"uart": "loaded_host_uart",
|
"uart": "loaded_host_uart",
|
||||||
"uart_opto": "loaded_hil_opto",
|
"uart_opto": "loaded_hil_opto",
|
||||||
"uart_can": "loaded_hil_can",
|
"uart_can": "loaded_hil_can",
|
||||||
"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:
|
||||||
|
|
|
||||||
|
|
@ -14,28 +14,28 @@ 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"))
|
|
||||||
|
|
||||||
|
|
||||||
POWER_ON_SETTLE_S = float(_get("HIL_TARGET_POWER_SETTLE_S", "1.5"))
|
POWER_ON_SETTLE_S = float(_get("HIL_TARGET_POWER_SETTLE_S", "1.5"))
|
||||||
|
|
||||||
# VCOM на программаторе NXP MCU-Link
|
# VCOM на программаторе NXP MCU-Link
|
||||||
VCOM_PORT: str = _get("HIL_VCOM_PORT", "/dev/ttyACM0")
|
VCOM_PORT: str = _get("HIL_VCOM_PORT", "/dev/ttyACM0")
|
||||||
VCOM_BAUD: int = int(_get("HIL_VCOM_BAUD", "115200"))
|
VCOM_BAUD: int = int(_get("HIL_VCOM_BAUD", "115200"))
|
||||||
READY_TIMEOUT: float = float(_get("HIL_READY_TIMEOUT", "5.0"))
|
READY_TIMEOUT: float = float(_get("HIL_READY_TIMEOUT", "5.0"))
|
||||||
|
|
||||||
# M5Stack StamPLC (промежуточная платформа для HIL)
|
# M5Stack StamPLC (промежуточная платформа для HIL)
|
||||||
M5_PORT: str = _get("HIL_M5_PORT", "/dev/ttyACM1")
|
M5_PORT: str = _get("HIL_M5_PORT", "/dev/ttyACM1")
|
||||||
M5_BAUD: int = int(_get("HIL_M5_BAUD", "115200"))
|
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"))
|
||||||
|
|
|
||||||
|
|
@ -48,16 +48,18 @@ 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")))
|
||||||
FCB_PATH = Path(_env("FCB_PATH", str(REPO_ROOT / "tools/host/dcd/w25q128_fdcb.bin")))
|
FCB_PATH = Path(_env("FCB_PATH", str(REPO_ROOT / "tools/host/dcd/w25q128_fdcb.bin")))
|
||||||
if not FCB_PATH.is_absolute():
|
if not FCB_PATH.is_absolute():
|
||||||
FCB_PATH = REPO_ROOT / FCB_PATH
|
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 располагается по смещению 0x1000 от начала Flash (ivtOffset в HAB yaml)
|
||||||
IVT_OFFSET = 0x1000
|
IVT_OFFSET = 0x1000
|
||||||
|
|
@ -65,12 +67,13 @@ IVT_OFFSET = 0x1000
|
||||||
# Маппинг имён прошивок → имена HAB-файлов (генерируются build::hab-*)
|
# Маппинг имён прошивок → имена HAB-файлов (генерируются build::hab-*)
|
||||||
HAB_NAMES = {
|
HAB_NAMES = {
|
||||||
"firmware_test": "firmware_test_hab.bin",
|
"firmware_test": "firmware_test_hab.bin",
|
||||||
"bootloader": "bootloader_hab.bin",
|
"bootloader": "bootloader_hab.bin",
|
||||||
"app": "app_hab.bin",
|
"app": "app_hab.bin",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── 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()
|
||||||
|
|
@ -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
|
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" FCB: {len(fcb):>6} bytes @ 0x60000000")
|
||||||
print(f" Padding: {len(padding):>6} bytes @ 0x{0x60000000 + len(fcb):08X}")
|
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:
|
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,10 +120,11 @@ 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). "
|
||||||
"Prepends FCB to HAB image before programming.",
|
"Prepends FCB to HAB image before programming.",
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
epilog=__doc__,
|
epilog=__doc__,
|
||||||
)
|
)
|
||||||
|
|
@ -170,7 +181,7 @@ def main() -> int:
|
||||||
if not hab_path.exists():
|
if not hab_path.exists():
|
||||||
print(f" ❌ HAB image not found: {hab_path}", file=sys.stderr)
|
print(f" ❌ HAB image not found: {hab_path}", file=sys.stderr)
|
||||||
fw_slug = args.firmware.replace("_", "-")
|
fw_slug = args.firmware.replace("_", "-")
|
||||||
bt_slug = args.build_type.lower()
|
bt_slug = args.build_type.lower()
|
||||||
print(
|
print(
|
||||||
f" Hint: run just build::hab-{fw_slug}-{bt_slug} "
|
f" Hint: run just build::hab-{fw_slug}-{bt_slug} "
|
||||||
"inside devcontainer first.",
|
"inside devcontainer first.",
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,15 @@ import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# ─── Пути ─────────────────────────────────────────────────────────────────────
|
# ─── Пути ─────────────────────────────────────────────────────────────────────
|
||||||
SCRIPT_DIR = Path(__file__).parent.resolve()
|
SCRIPT_DIR = Path(__file__).parent.resolve()
|
||||||
REPO_ROOT = SCRIPT_DIR.parent.parent
|
REPO_ROOT = SCRIPT_DIR.parent.parent
|
||||||
FLASHLOADER = SCRIPT_DIR / "dcd" / "ivt_flashloader.bin"
|
FLASHLOADER = SCRIPT_DIR / "dcd" / "ivt_flashloader.bin"
|
||||||
|
|
||||||
# BUILD_DIR: берём из окружения (just экспортирует из .env как абсолютный путь),
|
# BUILD_DIR: берём из окружения (just экспортирует из .env как абсолютный путь),
|
||||||
# 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,23 +51,24 @@ 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
|
|
||||||
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
|
# FlexSPI NOR config option word: 0xC0000007
|
||||||
# bits[31:28]=0xC — tag (QuadSPI NOR)
|
# bits[31:28]=0xC — tag (QuadSPI NOR)
|
||||||
# bits[3:0]=0x7 — option size
|
# bits[3:0]=0x7 — option size
|
||||||
FLEXSPI_OPTION_ADDR = "0x2000"
|
FLEXSPI_OPTION_ADDR = "0x2000"
|
||||||
FLEXSPI_OPTION_VALUE = "0xC0000007"
|
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
|
# Option word для записи FCB: tag=0xF → Write FCB command
|
||||||
FLEXSPI_FCB_VALUE = "0xF000000F"
|
FLEXSPI_FCB_VALUE = "0xF000000F"
|
||||||
|
|
||||||
# Flash layout
|
# Flash layout
|
||||||
FLASH_BASE = 0x60000000
|
FLASH_BASE = 0x60000000
|
||||||
HAB_OFFSET = 0x1000 # IVT offset: write address = FLASH_BASE + HAB_OFFSET
|
HAB_OFFSET = 0x1000 # IVT offset: write address = FLASH_BASE + HAB_OFFSET
|
||||||
|
|
||||||
|
|
||||||
def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess:
|
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:
|
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(
|
||||||
choices=["firmware_test", "bootloader", "app"],
|
"--firmware",
|
||||||
help="Имя прошивки")
|
required=True,
|
||||||
parser.add_argument("--build-type", required=True,
|
choices=["firmware_test", "bootloader", "app"],
|
||||||
choices=["Debug", "Release"],
|
help="Имя прошивки",
|
||||||
help="Тип сборки")
|
)
|
||||||
parser.add_argument("--ram-only", action="store_true",
|
parser.add_argument(
|
||||||
help="Загрузить в RAM без записи во Flash")
|
"--build-type", required=True, choices=["Debug", "Release"], help="Тип сборки"
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue