# service_tui: fix 1 force written QE=1 bit FlexSPI W25Q
This commit is contained in:
parent
49ae714955
commit
1d9055628a
6 changed files with 501 additions and 69 deletions
|
|
@ -64,7 +64,7 @@ just host::flash-production # bootloader release + app release (с подт
|
|||
flowchart TD
|
||||
A["Плата в SDP-режиме\n1FC9:0130"] --> B["sdphost\nзагрузить ivt_flashloader.bin\nв RAM 0x20001C00"]
|
||||
B --> C["sdphost jump-address\nFlashloader поднимается\nкак 15A2:0073"]
|
||||
C --> D["configure-memory 0xC0000007\nинициализация FlexSPI NOR"]
|
||||
C --> D["configure-memory 0xC0000207\nинициализация FlexSPI NOR + QE-бит"]
|
||||
D --> E["flash-erase-region 0x60000000"]
|
||||
E --> F["configure-memory 0xF000000F\nзапись FCB в 0x60000000"]
|
||||
F --> G["write-memory 0x60001000\nHAB-образ"]
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@ BootROM в режиме SDP умеет только писать в RAM и пр
|
|||
│
|
||||
Flashloader запущен
|
||||
│ blhost -u 0x15A2,0x0073
|
||||
├─ fill-memory 0x2000 4 0xC0000007 ← option word для FlexSPI NOR
|
||||
├─ fill-memory 0x2000 4 0xC0000207 ← option word для FlexSPI NOR (+QE-бит)
|
||||
├─ configure-memory 9 0x2000 ← Flashloader пишет FCB в Flash
|
||||
├─ flash-erase-region 0x60000000 …
|
||||
├─ write-memory 0x60002000 firmware_hab.bin ← HAB образ
|
||||
|
|
|
|||
|
|
@ -62,11 +62,19 @@ SDP_USB = _usb("BOOTROM_VID", "1fc9", "BOOTROM_PID", "0130")
|
|||
BLHOST_USB = _usb("FLASHLOADER_VID", "15a2", "FLASHLOADER_PID", "0073")
|
||||
|
||||
# ─── Аппаратные константы ─────────────────────────────────────────────────────
|
||||
# FlexSPI NOR config option word: 0xC0000007
|
||||
# FlexSPI NOR config option word: 0xC0000207
|
||||
# bits[31:28]=0xC — tag (QuadSPI NOR)
|
||||
# bits[3:0]=0x7 — option size
|
||||
# bits[11:8]=0x2 — quad_mode_setting: установить QE-бит (Status Register 2
|
||||
# bit 1, формат Winbond W25Q). КРИТИЧНО: часть партий
|
||||
# W25Q128 приходит с завода с QE=0 — без этого поля (было
|
||||
# 0xC0000007) любая flash-операция на них падает с
|
||||
# FlexSPINOR Command Failure. QE энергонезависимый; для
|
||||
# чипов с уже установленным QE — no-op. Значение сверено
|
||||
# с NXP MCUBootUtility (память W25Q). См. service_tui
|
||||
# flash_backend.py, docstring Р15.
|
||||
# bits[3:0]=0x7 — maxFreq
|
||||
FLEXSPI_OPTION_ADDR = "0x2000"
|
||||
FLEXSPI_OPTION_VALUE = "0xC0000007"
|
||||
FLEXSPI_OPTION_VALUE = "0xC0000207"
|
||||
FLEXSPI_MEMORY_ID = "9" # FlexSPI NOR memory interface ID
|
||||
|
||||
# Option word для записи FCB: tag=0xF → Write FCB command
|
||||
|
|
|
|||
|
|
@ -21,6 +21,110 @@ Zero Textual/asyncio импортов — модуль полностью син
|
|||
закрывается в finally на любом исходе (защита от утечки HID-хэндла в
|
||||
редком окне «wait_for_flashloader вернул интерфейс → USB выдернут →
|
||||
McuBoot.__enter__ упал»).
|
||||
|
||||
Р13 (полевой баг, ~50/500 плат — flash_erase_region/flash_erase_all
|
||||
возвращали False с «устройство пропало с шины», хотя плата оставалась на
|
||||
месте и стиралась штатно через NXP MCUBootUtility): два независимых бага.
|
||||
(1) _fail_command() проверял присутствие BootROM SDP, а к моменту вызова
|
||||
плата уже спрыгнула на Flashloader (см. load_flashloader()) — проверка
|
||||
гарантированно возвращала «нет» независимо от реального состояния платы,
|
||||
любой False от Flashloader-команды маскировался под обрыв USB. (2)
|
||||
flash()/flash_erase_region и configure_flexspi() (в обоих путях) работали
|
||||
на spsdk-дефолтном HID read-таймауте 2000мс (UsbDevice.__init__) — erase_chip()
|
||||
поднимал таймаут только перед flash_erase_all, уже ПОСЛЕ configure_flexspi().
|
||||
McuBoot(iface) создаётся с cmd_exception=False (дефолт) — таймаут не бросает
|
||||
исключение, а тихо оседает в status_code=NO_RESPONSE, поэтому команда просто
|
||||
возвращает False без трассировки. На части плат (полевые логи: ~2с до отказа
|
||||
на flash_erase_region) реальный ответ не укладывался в 2с. Фикс: единый
|
||||
MCUBOOT_CMD_TIMEOUT_MS выставляется сразу после load_flashloader(), до первой
|
||||
команды сессии; _fail_command() проверяет detect_flashloader() и добавляет
|
||||
mboot.status_string в текст ошибки — отличить в логе «host не дождался
|
||||
ответа» (NoResponse) от настоящего кода ошибки устройства.
|
||||
|
||||
Р13, продолжение (после фикса выше status_string на живом железе показал
|
||||
НАСТОЯЩУЮ причину — не таймаут, не обрыв связи): flash_erase_region/
|
||||
flash_erase_all возвращают status 20106 «FlexSPINOR: Command Failure» —
|
||||
дженерик-ошибка исполнения команды контроллером FlexSPI (НЕ 20101/20102
|
||||
EraseSectorFail/EraseAllFail — до собственно попытки стереть дело не
|
||||
доходит), причина пока НЕ найдена (см. ниже — одна из гипотез уже
|
||||
опровергнута на железе).
|
||||
|
||||
Р13, ОПРОВЕРГНУТАЯ гипотеза (не повторять без новых данных): сверка с
|
||||
boot_utility_log.txt (NXP MCUBootUtility, erase на той же плате проходит
|
||||
штатно) показала порядок configure-memory(0xC0000007) [probe] →
|
||||
configure-memory(0xF000000F) [commit FCB] → flash-erase-all — у нас
|
||||
write_fcb_auto() (0xF000000F) либо не вызывался перед erase вообще
|
||||
(erase_chip), либо вызывался только после (flash). Гипотеза была: commit
|
||||
нужен ДО erase, чтобы контроллер полностью сконфигурировался под
|
||||
erase-команды, а физически записанные им байты «всё равно сотрутся»
|
||||
следующей erase-командой. **Это ПРОВЕРЕНО и ОПРОВЕРГНУТО на железе**:
|
||||
добавление write_fcb_auto(mboot) сразу после configure_flexspi(mboot), до
|
||||
erase, в обоих путях, дало НОВУЮ ошибку — status 10203 «Memory Cumulative
|
||||
Write» (см. error_codes.py: соседний код IAP_CUMULATIVE_WRITE =
|
||||
«Flash Memory Region To Be Programmed Is Not Empty») — И СЛОМАЛО ранее
|
||||
рабочую плату. Значит configure-memory(0xF000000F) — это не «донастройка
|
||||
контроллера», а НЕМЕДЛЕННАЯ физическая запись FCB во flash прямо в момент
|
||||
вызова; запись в НЕ стёртую область (то есть в любую ранее прошитую плату)
|
||||
эту запись сразу проваливает — она не успевает дождаться erase. Правка
|
||||
отменена (revert). Почему в логе NXP это работало — неизвестно: либо их
|
||||
0xF000000F-эквивалент в этой конкретной последовательности целится в уже
|
||||
стёртую область по другой причине, либо это вообще не то, чем кажется на
|
||||
первый взгляд (лог обрывается ровно на flash-erase-all, что было ДО или
|
||||
ПОСЛЕ него в их полном сценарии — не зафиксировано). Не полагаться на
|
||||
сопоставление логов двух разных инструментов без подтверждения по
|
||||
официальной документации NXP (AN12107) или доступа к ROM-исходникам.
|
||||
|
||||
Р14 (переоценка после доп. вопросов пользователю): AN12107 не описывает
|
||||
FCB/configure-memory вообще (проверено — pdftotext + grep по всему файлу,
|
||||
0 совпадений). Форум NXP (RT1064) прямо говорит: configure-memory(0xC0000007)
|
||||
«используется только для установления соединения» (речь идёт про скорость
|
||||
FlexSPI), а не для настройки конкретного чипа — версия про «нужен pre-existing
|
||||
корректный FCB на чипе» отклонена САМИМ пользователем: платы приходят с
|
||||
завода с гарантированно пустым внешним flash, поэтому у «рабочих» 450 плат
|
||||
просто не может быть валидного FCB к моменту первого касания. Уточняющие
|
||||
вопросы дали решающий факт: на ~50 проблемных платах **обычная** прошивка
|
||||
firmware_test (не только erase/«Другое») тоже падает, и чип/ревизия платы —
|
||||
**та же**, что у 450 рабочих (не другая партия/поставщик). Значит проблема не
|
||||
в логике конфигурации FlexSPI и не в конкретном чипе — она проявляется на
|
||||
случайном подмножестве физически идентичных плат независимо от того, какая
|
||||
именно flash-команда выполняется первой. Это типичная картина маргинального
|
||||
электрического контакта (пайка/разводка/питание конкретного экземпляра),
|
||||
воспроизводимого на erase (самая «тяжёлая» операция по току/длительности —
|
||||
NOR erase требует внутренней подкачки напряжения), а не логической ошибки
|
||||
хоста. Софт не может физически починить плохую пайку, но может дать шанс
|
||||
операции пройти со второй/третьей попытки, если условие временное. Фикс:
|
||||
retry (см. _run_flash_cmd) вокруг каждой команды, реально трогающей flash-чип
|
||||
(configure_memory/erase/write) — если контакт временно «плавает», повторная
|
||||
попытка через паузу может пройти там, где первая не удалась; если ошибка
|
||||
детерминированная (чип действительно неисправен), retry её не замаскирует —
|
||||
просто несколько раз повторит тот же честный отказ перед тем, как сдаться.
|
||||
[Р15: retry не помог — все 3 попытки падали с тем же 20106. Гипотеза
|
||||
«маргинальный контакт» опровергнута доп. фактами: MCUBootUtility на тех же
|
||||
платах работает 10/10, а после ОДНОГО её касания плата НАВСЕГДА начинает
|
||||
работать и в нашей утилите. Это не электрика — это энергонезависимое
|
||||
состояние чипа. Retry оставлен как безвредная страховка.]
|
||||
|
||||
Р15 — НАЙДЕННАЯ ПРИЧИНА (misread лога в Р13): в логах MCUBootUtility
|
||||
option word — десятичное 3221225991 = 0xC0000207, а НЕ 0xC0000007, как было
|
||||
ошибочно прочитано при сверке в Р13 (и как стоило у нас с Фазы 0, унаследовано
|
||||
от flash_usb.py). Разница — поле quad_mode_setting (биты [11:8]): 2 вместо 0.
|
||||
quad_mode_setting=2 велит flashloader'у установить QE-бит (Quad Enable,
|
||||
Status Register 2 bit 1 — формат Winbond) на самом чипе при configure-memory.
|
||||
QE у Winbond ЭНЕРГОНЕЗАВИСИМЫЙ, и часть партий W25Q128 приходит с завода с
|
||||
QE=0 (Winbond выпускает варианты с заводским QE=0 и QE=1 под почти одинаковой
|
||||
маркировкой). Это объясняет ВСЁ наблюдённое детерминированно:
|
||||
- на чипах с QE=0 наш 0xC0000007 не включал quad-режим, LUT настроен на
|
||||
quad-команды → чип не отвечает → 20106 Command Failure на ЛЮБОЙ операции
|
||||
(erase/обычная прошивка/«Другое») — ровно как в поле (~50/500 плат);
|
||||
- MCUBootUtility (выбор W25Q в External Memory → 0xC0000207) работает
|
||||
10/10 и, единожды установив QE=1, НАВСЕГДА «чинит» плату и для нас;
|
||||
- на 450 «рабочих» платах — чипы из партий с заводским QE=1;
|
||||
- форумный лог RT1064 с 0xC0000007 не противоречит: MCUBootUtility
|
||||
подставляет option word по выбранному чипу, там чип другой.
|
||||
Для чипов с уже установленным QE повторная установка — no-op (это статус-
|
||||
регистр, не массив — Cumulative Write здесь не существует). Урок: десятичные
|
||||
значения в чужих логах пересчитывать инструментом, не «на глаз» — misread
|
||||
одного слова стоил трёх раундов на живом железе, включая один регресс.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -109,13 +213,36 @@ HAB_OFFSET = 0x1000
|
|||
FLASHLOADER_LOAD_ADDR = 0x20001C00
|
||||
|
||||
FLEXSPI_OPTION_ADDR = 0x2000
|
||||
FLEXSPI_OPTION_VALUE = 0xC0000007
|
||||
# Р15: 0xC0000207, НЕ 0xC0000007. Поле quad_mode_setting (биты [11:8]) = 2 →
|
||||
# flashloader при configure-memory устанавливает QE-бит (Quad Enable, Status
|
||||
# Register 2 bit 1 — формат Winbond W25Q) на самом flash-чипе. QE у Winbond
|
||||
# энергонезависимый; часть партий W25Q128 приходит с завода с QE=0 — на них
|
||||
# quad_mode_setting=0 («QE не трогать») оставлял чип в SPI-режиме при
|
||||
# LUT-таблицах, настроенных на quad-команды → ЛЮБАЯ операция (erase/write)
|
||||
# падала с 20106 FlexSPINOR Command Failure. Значение снято с живого лога
|
||||
# NXP MCUBootUtility (fill-memory ... 3221225991 = 0xC0000207 при выбранной
|
||||
# памяти W25Q). Для чипов с уже установленным QE повторная установка — no-op.
|
||||
FLEXSPI_OPTION_VALUE = 0xC0000207
|
||||
FLEXSPI_FCB_VALUE = 0xF000000F # tag=0xF → Write FCB command (auto-config)
|
||||
FLEXSPI_MEMORY_ID = 9
|
||||
|
||||
ERASE_ALL_TIMEOUT_MS = 200_000 # эквивалент blhost -t 200000 (см. Фазу 0, ⚠В2)
|
||||
# Единый read/write-таймаут HID-команд на всю Flashloader-сессию (configure +
|
||||
# erase + write) — см. Р13. spsdk-дефолт 2000мс (UsbDevice.__init__) слишком
|
||||
# короткий для части плат в поле; значение — эквивалент blhost -t 200000.
|
||||
MCUBOOT_CMD_TIMEOUT_MS = 200_000
|
||||
FLASHLOADER_WAIT_TIMEOUT_S = 10.0
|
||||
|
||||
# Р14/Р15 — повторные попытки flash-команды. Изначально добавлены под
|
||||
# гипотезу «маргинальный контакт», которая позже опроверглась (настоящая
|
||||
# причина — QE-бит, Р15). Оставлены как безвредная defensive-страховка от
|
||||
# РЕАЛЬНО транзиентных сбоев USB/HID: happy-path не затрагивают (первая же
|
||||
# успешная попытка возвращает управление), детерминированную ошибку не
|
||||
# маскируют (после N попыток — тот же честный _fail_command со status_string).
|
||||
# Компромисс: на действительно битой плате erase теперь может тянуться до
|
||||
# N×~54с перед отказом — приемлемо, такие платы редки. См. модульный docstring.
|
||||
CMD_RETRY_ATTEMPTS = 3
|
||||
CMD_RETRY_DELAY_S = 0.5
|
||||
|
||||
_HAB_OPTIONS_TEMPLATE = [
|
||||
"options:",
|
||||
" flags: 0x00",
|
||||
|
|
@ -198,33 +325,80 @@ def detect_cdc() -> bool:
|
|||
return resolve_serial_port(UsbId(_CDC_VID, _CDC_PID)) is not None
|
||||
|
||||
|
||||
def _sdp_still_present() -> bool:
|
||||
def detect_flashloader() -> bool:
|
||||
"""True если виден Flashloader (см. _FLASHLOADER_DEVICE_ID)."""
|
||||
return len(MbootUSBInterface.scan(device_id=_FLASHLOADER_DEVICE_ID)) > 0
|
||||
|
||||
|
||||
def _flashloader_still_present() -> bool:
|
||||
"""Быстрая проверка «плата ещё на шине» для error-путей (вариант B, Р10).
|
||||
|
||||
Проверяет присутствие Flashloader, НЕ BootROM SDP (Р13, исторический
|
||||
баг): все вызовы _fail_command() приходят из команд McuBoot
|
||||
(configure_flexspi/write_fcb_*/flash_erase_*/write_memory), которые
|
||||
в принципе выполняются только после того, как плата спрыгнула с SDP на
|
||||
Flashloader внутри load_flashloader() — проверка по SDP тут возвращала
|
||||
бы «нет» ВСЕГДА, независимо от реального состояния платы.
|
||||
|
||||
Любая ошибка самой проверки трактуется как «устройства нет»: проверка
|
||||
выполняется только ПОСЛЕ уже случившегося сбоя команды, шина в этот
|
||||
момент нестабильна, и «не смог проверить» практически всегда означает
|
||||
«плату выдернули» (согласовано, RELEASE_ROADMAP.md §B).
|
||||
"""
|
||||
try:
|
||||
return detect_sdp()
|
||||
return detect_flashloader()
|
||||
except Exception: # noqa: BLE001 — см. docstring: любой сбой ⇒ считаем обрывом
|
||||
return False
|
||||
|
||||
|
||||
def _fail_command(message: str) -> None:
|
||||
def _fail_command(mboot: McuBoot, message: str) -> None:
|
||||
"""Живая команда spsdk вернула False — переклассификация по варианту B (Р10).
|
||||
|
||||
Если устройство пропало с шины → обрыв (ConnectionLostError), иначе →
|
||||
честная ошибка операции (FlashBackendError с прежним текстом). detect_sdp()
|
||||
честная ошибка операции (FlashBackendError). detect_flashloader()
|
||||
выполняется ТОЛЬКО здесь, в error-пути; happy path не затрагивается.
|
||||
mboot.status_string добавляется к тексту ошибки во всех случаях (Р13) —
|
||||
настоящий код статуса spsdk (например «NoResponse» при хостовом
|
||||
read-таймауте) отличим от кода ошибки, реально сообщённого устройством.
|
||||
|
||||
:raises ConnectionLostError: устройство исчезло с шины после сбоя команды.
|
||||
:raises FlashBackendError: устройство на месте — ошибка самой операции.
|
||||
"""
|
||||
if not _sdp_still_present():
|
||||
raise ConnectionLostError(f"{message} (устройство пропало с шины)")
|
||||
raise FlashBackendError(message)
|
||||
full_message = f"{message} (status: {mboot.status_string})"
|
||||
if not _flashloader_still_present():
|
||||
raise ConnectionLostError(f"{full_message} (устройство пропало с шины)")
|
||||
raise FlashBackendError(full_message)
|
||||
|
||||
|
||||
def _run_flash_cmd(mboot: McuBoot, description: str, cmd: Callable[[], bool]) -> None:
|
||||
"""Выполняет одну flash-команду (configure_memory/erase/write) с повторами.
|
||||
|
||||
Р14: часть плат в поле отдаёт `status FlexSPINOR Command Failure` на
|
||||
случайной операции с flash-чипом — похоже на маргинальный электрический
|
||||
контакт (пайка/питание конкретного экземпляра), а не на логическую
|
||||
ошибку хоста. Если условие временное, повтор через паузу может пройти
|
||||
там, где первая попытка не удалась; если ошибка детерминированная
|
||||
(плата действительно неисправна), повтор её не замаскирует — после
|
||||
исчерпания попыток ошибка всё равно уходит в _fail_command() с честным
|
||||
текстом. Между попытками проверяется присутствие Flashloader — при
|
||||
реальном обрыве связи retry не имеет смысла, сразу же уходим в
|
||||
_fail_command() (см. ConnectionLostError).
|
||||
|
||||
:raises ConnectionLostError: устройство исчезло с шины между попытками.
|
||||
:raises FlashBackendError: команда не удалась во всех попытках,
|
||||
устройство на месте.
|
||||
"""
|
||||
for attempt in range(1, CMD_RETRY_ATTEMPTS + 1):
|
||||
if cmd():
|
||||
return
|
||||
if attempt < CMD_RETRY_ATTEMPTS and _flashloader_still_present():
|
||||
logger.warning(
|
||||
f"{description}: попытка {attempt}/{CMD_RETRY_ATTEMPTS} не удалась "
|
||||
f"(status: {mboot.status_string}), повтор через {CMD_RETRY_DELAY_S}с"
|
||||
)
|
||||
time.sleep(CMD_RETRY_DELAY_S)
|
||||
continue
|
||||
_fail_command(mboot, f"{description} вернул False после {attempt} попыт(ок)")
|
||||
|
||||
|
||||
def _emit(
|
||||
|
|
@ -318,11 +492,28 @@ def load_flashloader(
|
|||
|
||||
|
||||
def configure_flexspi(mboot: McuBoot) -> None:
|
||||
"""Инициализирует FlexSPI NOR контроллер (см. flash_usb.py::configure_flexspi)."""
|
||||
"""Инициализирует FlexSPI NOR контроллер + включает QE-бит чипа (Р15).
|
||||
|
||||
Option word 0xC0000207 (см. комментарий у константы) — flashloader
|
||||
пробует чип по SFDP и, что критично, устанавливает энергонезависимый
|
||||
QE-бит на чипах, пришедших с завода с QE=0. Второе option-слово
|
||||
обнуляется явно — зеркало последовательности NXP MCUBootUtility
|
||||
(при optionSize=0 оно не должно читаться, но страхуемся от мусора в RAM).
|
||||
|
||||
ВАЖНО для «Другое»: flash() зовёт configure_flexspi() ВСЕГДА, в т.ч. для
|
||||
custom-бинарей с явным FCB. quad_mode_setting=2 = «QE через Status
|
||||
Register 2 bit 1, команда 0x31» — это метод Winbond. Все текущие
|
||||
FcbVariant — Winbond W25Q, для них корректно. Если в список памяти когда-
|
||||
нибудь добавят не-Winbond чип (другой метод QE — SR1 bit 6, отдельный
|
||||
регистр и т.п.), этот option word под него нужно будет пересмотреть.
|
||||
"""
|
||||
mboot.fill_memory(FLEXSPI_OPTION_ADDR, 4, FLEXSPI_OPTION_VALUE)
|
||||
ok = mboot.configure_memory(FLEXSPI_OPTION_ADDR, FLEXSPI_MEMORY_ID)
|
||||
if not ok:
|
||||
_fail_command("configure_memory (FlexSPI init) вернул False")
|
||||
mboot.fill_memory(FLEXSPI_OPTION_ADDR + 4, 4, 0)
|
||||
_run_flash_cmd(
|
||||
mboot,
|
||||
"configure_memory (FlexSPI init)",
|
||||
lambda: mboot.configure_memory(FLEXSPI_OPTION_ADDR, FLEXSPI_MEMORY_ID),
|
||||
)
|
||||
|
||||
|
||||
def write_fcb_auto(mboot: McuBoot) -> None:
|
||||
|
|
@ -331,9 +522,11 @@ def write_fcb_auto(mboot: McuBoot) -> None:
|
|||
Надёжно проверен только для W25Q128 — см. docstring оригинала.
|
||||
"""
|
||||
mboot.fill_memory(FLEXSPI_OPTION_ADDR, 4, FLEXSPI_FCB_VALUE)
|
||||
ok = mboot.configure_memory(FLEXSPI_OPTION_ADDR, FLEXSPI_MEMORY_ID)
|
||||
if not ok:
|
||||
_fail_command("configure_memory (FCB write) вернул False")
|
||||
_run_flash_cmd(
|
||||
mboot,
|
||||
"configure_memory (FCB write)",
|
||||
lambda: mboot.configure_memory(FLEXSPI_OPTION_ADDR, FLEXSPI_MEMORY_ID),
|
||||
)
|
||||
|
||||
|
||||
def write_fcb_explicit(mboot: McuBoot, fcb_path: Path) -> None:
|
||||
|
|
@ -345,9 +538,11 @@ def write_fcb_explicit(mboot: McuBoot, fcb_path: Path) -> None:
|
|||
if not fcb_path.exists():
|
||||
raise FlashBackendError(f"FCB-файл не найден: {fcb_path}")
|
||||
data = fcb_path.read_bytes()
|
||||
ok = mboot.write_memory(FLASH_BASE, data, mem_id=0)
|
||||
if not ok:
|
||||
_fail_command(f"write_memory(FCB {fcb_path.name}) вернул False")
|
||||
_run_flash_cmd(
|
||||
mboot,
|
||||
f"write_memory(FCB {fcb_path.name})",
|
||||
lambda: mboot.write_memory(FLASH_BASE, data, mem_id=0),
|
||||
)
|
||||
|
||||
|
||||
# ─── Прошивка / RAM-load / erase ────────────────────────────────────────────
|
||||
|
|
@ -391,6 +586,7 @@ def flash(
|
|||
return
|
||||
|
||||
iface = load_flashloader(progress_cb)
|
||||
iface.device.timeout = MCUBOOT_CMD_TIMEOUT_MS # см. Р13 — до первой команды
|
||||
|
||||
write_addr = FLASH_BASE + HAB_OFFSET
|
||||
hab_size = hab_bin.stat().st_size
|
||||
|
|
@ -410,9 +606,11 @@ def flash(
|
|||
0,
|
||||
f"Стирание 0x{FLASH_BASE:08X} + {erase_size} байт",
|
||||
)
|
||||
ok = mboot.flash_erase_region(FLASH_BASE, erase_size, mem_id=0)
|
||||
if not ok:
|
||||
_fail_command("flash_erase_region вернул False")
|
||||
_run_flash_cmd(
|
||||
mboot,
|
||||
"flash_erase_region",
|
||||
lambda: mboot.flash_erase_region(FLASH_BASE, erase_size, mem_id=0),
|
||||
)
|
||||
|
||||
_emit(progress_cb, "fcb", 0, "Запись FCB")
|
||||
if fcb_path is not None:
|
||||
|
|
@ -427,11 +625,13 @@ def flash(
|
|||
_emit(progress_cb, "write", percent, f"{current}/{total} байт")
|
||||
|
||||
data = hab_bin.read_bytes()
|
||||
ok = mboot.write_memory(
|
||||
write_addr, data, mem_id=0, progress_callback=_on_progress
|
||||
_run_flash_cmd(
|
||||
mboot,
|
||||
"write_memory (HAB-образ)",
|
||||
lambda: mboot.write_memory(
|
||||
write_addr, data, mem_id=0, progress_callback=_on_progress
|
||||
),
|
||||
)
|
||||
if not ok:
|
||||
_fail_command("write_memory (HAB-образ) вернул False")
|
||||
|
||||
_emit(progress_cb, "reset", 0, "Reset")
|
||||
mboot.reset(reopen=False)
|
||||
|
|
@ -452,6 +652,7 @@ def erase_chip(progress_cb: Optional[ProgressCallback] = None) -> None:
|
|||
:raises ConnectionLostError: обрыв USB посреди chip erase.
|
||||
"""
|
||||
iface = load_flashloader(progress_cb)
|
||||
iface.device.timeout = MCUBOOT_CMD_TIMEOUT_MS # см. Р13 — уже для configure_flexspi
|
||||
|
||||
try:
|
||||
with McuBoot(iface) as mboot:
|
||||
|
|
@ -459,10 +660,11 @@ def erase_chip(progress_cb: Optional[ProgressCallback] = None) -> None:
|
|||
configure_flexspi(mboot)
|
||||
|
||||
_emit(progress_cb, "erase", 0, "Полная очистка Flash (~30с)")
|
||||
iface.device.timeout = ERASE_ALL_TIMEOUT_MS # см. Фазу 0, ⚠В2
|
||||
ok = mboot.flash_erase_all(mem_id=FLEXSPI_MEMORY_ID)
|
||||
if not ok:
|
||||
_fail_command("flash_erase_all вернул False")
|
||||
_run_flash_cmd(
|
||||
mboot,
|
||||
"flash_erase_all",
|
||||
lambda: mboot.flash_erase_all(mem_id=FLEXSPI_MEMORY_ID),
|
||||
)
|
||||
|
||||
_emit(progress_cb, "reset", 0, "Reset")
|
||||
mboot.reset(reopen=False)
|
||||
|
|
|
|||
|
|
@ -288,13 +288,30 @@ FlashBackendError (connection_lost: bool = False)
|
|||
`erase_chip`.
|
||||
- **Вариант B для команд, возвращающих `False` без исключения** (Р10,
|
||||
`_fail_command()`): `flash_erase_region`/`flash_erase_all`/`write_memory`
|
||||
иногда просто возвращают `False` вместо исключения. В этом случае
|
||||
`_fail_command()` выполняет быстрый `_sdp_still_present()` (обёрнутый в
|
||||
`try/except` — любая ошибка самой проверки трактуется как «устройства
|
||||
нет», т.к. шина к этому моменту уже нестабильна): устройство пропало →
|
||||
`ConnectionLostError`, устройство на месте → обычный `FlashBackendError` с
|
||||
текстом ошибки операции. Проверка добавляется **только в error-путь**, на
|
||||
happy path не влияет.
|
||||
иногда просто возвращают `False` вместо исключения (`McuBoot(iface)`
|
||||
создаётся с `cmd_exception=False` — дефолт spsdk, статус оседает в
|
||||
`status_code`, исключения нет). В этом случае `_fail_command()` выполняет
|
||||
быстрый `_flashloader_still_present()` (обёрнутый в `try/except` — любая
|
||||
ошибка самой проверки трактуется как «устройства нет», т.к. шина к этому
|
||||
моменту уже нестабильна): устройство пропало → `ConnectionLostError`,
|
||||
устройство на месте → обычный `FlashBackendError` с текстом ошибки
|
||||
операции + `mboot.status_string`. Проверка добавляется **только в
|
||||
error-путь**, на happy path не влияет.
|
||||
**Р13 (полевой баг, ~50/500 плат):** до фикса эта проверка звала
|
||||
`_sdp_still_present()` — сканировала BootROM SDP VID:PID, хотя к моменту
|
||||
вызова `_fail_command()` плата уже спрыгнула на Flashloader (другой
|
||||
VID:PID) внутри `load_flashloader()`. Проверка возвращала «нет»
|
||||
**всегда**, независимо от реального состояния платы — любой `False` от
|
||||
Flashloader-команды маскировался под обрыв USB. Одновременно
|
||||
`flash()`/`flash_erase_region` и `configure_flexspi()` (в обоих путях) не
|
||||
поднимали `iface.device.timeout` вообще — работали на spsdk-дефолте
|
||||
2000мс (`UsbDevice.__init__`), тогда как NXP MCUBootUtility использует
|
||||
50–2048с на те же команды. На части плат в поле реальный ответ не
|
||||
укладывался в 2с → тихий `NO_RESPONSE` → `False` → неверно
|
||||
классифицировался как «устройство пропало с шины». Фикс: единый
|
||||
`MCUBOOT_CMD_TIMEOUT_MS` выставляется сразу после `load_flashloader()`,
|
||||
до первой команды сессии (не только перед `flash_erase_all`, как было
|
||||
раньше); `_fail_command()` проверяет `detect_flashloader()`, не SDP.
|
||||
- **`Flasher._run_flash_op()`** (`flasher.py`) конвертирует
|
||||
`FlashBackendError` обратно в `FlashResult(ok=False,
|
||||
connection_lost=exc.connection_lost)` + событие `FlashProgress(phase="error")`.
|
||||
|
|
@ -352,8 +369,9 @@ AppFrame {
|
|||
|
||||
Штатные HAB-образы (`firmware_test`/`bootloader`/`app`) собираются
|
||||
`nxpimage` заранее (`just build::hab-*`) и всегда идут на плату с W25Q128 —
|
||||
для них auto-config Flashloader (`configure-memory 0xC0000007` →
|
||||
`0xF000000F`, см. `HOW_TO_FLASH.md`) достаточен. Для сторонних/легаси
|
||||
для них auto-config Flashloader (`configure-memory 0xC0000207` →
|
||||
`0xF000000F`, см. `HOW_TO_FLASH.md`; до Р15 было `0xC0000007` — без
|
||||
установки QE-бита, см. §8.4) достаточен. Для сторонних/легаси
|
||||
бинарников (старые платы, W25Q256/512) это не так: auto-config Flashloader
|
||||
не документирован как надёжный для 4-байтной адресации, а сами бинарники
|
||||
приходят «сырыми» (код + таблица векторов, без FCB/IVT/DCD — тот же формат,
|
||||
|
|
@ -449,12 +467,72 @@ def write_fcb_explicit(mboot: McuBoot, fcb_path: Path) -> None:
|
|||
`fcb_path` в `flash_backend.flash()` — путь без него не тронут: штатная
|
||||
прошивка идёт через auto-config `write_fcb_auto`, как и раньше.
|
||||
|
||||
Таймаут для `flash_erase_all` (chip erase) увеличен до `ERASE_ALL_TIMEOUT_MS
|
||||
= 200_000` мс (эквивалент `blhost -t 200000`): W25Q512 стирается заметно
|
||||
дольше W25Q128, дефолтного таймаута McuBoot не хватало.
|
||||
`flash_erase_region` (стирание пары секторов под FCB+HAB при обычной
|
||||
прошивке) не трогали — там масштаб на порядки меньше, дефолта достаточно
|
||||
независимо от чипа.
|
||||
Таймаут HID-команд (`iface.device.timeout`) поднят до `MCUBOOT_CMD_TIMEOUT_MS
|
||||
= 200_000` мс (эквивалент `blhost -t 200000`) на всю Flashloader-сессию
|
||||
целиком — `configure_flexspi()`, `flash_erase_region`/`flash_erase_all`,
|
||||
`write_fcb_*`, `write_memory`, выставляется сразу после `load_flashloader()`,
|
||||
до первой команды. Изначально (Фаза 0) считалось, что `flash_erase_region`
|
||||
(пара секторов под FCB+HAB) укладывается в spsdk-дефолт 2000мс независимо от
|
||||
чипа и трогать его не нужно — на живом парке плат это оказалось не так
|
||||
(Р13, ~50/500 плат из полевых логов не укладывались в 2с), см. §6.2.
|
||||
|
||||
После фикса выше hardware-тест на «отбракованной» плате показал НАСТОЯЩУЮ
|
||||
причину сбоя вместо таймаута: `status: FlexSPINOR: Command Failure` (spsdk
|
||||
`StatusCode` 20106 — дженерик-ошибка исполнения команды контроллером
|
||||
FlexSPI, не специализированный `EraseSectorFail`/`EraseAllFail`). **Причина
|
||||
всё ещё не найдена.** Первая гипотеза (commit FCB `0xF000000F` нужен ДО
|
||||
erase, по аналогии с `boot_utility_log.txt` NXP MCUBootUtility) была
|
||||
**проверена на железе и опровергнута**: `configure-memory(0xF000000F)`
|
||||
оказался немедленной физической записью FCB во flash, а не безобидной
|
||||
донастройкой контроллера — на НЕ стёртой области (любая ранее прошитая
|
||||
плата) запись сразу проваливается со status 10203 «Memory Cumulative
|
||||
Write», и это сломало ранее рабочую плату. Правка отменена. Полная
|
||||
хронология и текст урока — в `flash_backend.py`, docstring, раздел «Р13,
|
||||
ОПРОВЕРГНУТАЯ гипотеза».
|
||||
|
||||
**Р14 — переоценка после дополнительных данных.** AN12107 не описывает
|
||||
FCB/configure-memory вообще (проверено, 0 совпадений по всему файлу).
|
||||
Уточняющие вопросы дали решающие факты: (1) на тех же ~50 проблемных платах
|
||||
падает и **обычная** прошивка firmware_test, не только erase/«Другое»; (2)
|
||||
чип и ревизия платы — **та же**, что у 450 рабочих, не другая партия. Значит
|
||||
дело не в конкретной flash-команде и не в чужеродном чипе — сбой проявляется
|
||||
на случайном подмножестве физически идентичных плат независимо от того,
|
||||
какая именно команда выполняется первой. Это картина маргинального
|
||||
электрического контакта конкретного экземпляра (пайка/питание), проявляется
|
||||
сильнее всего на erase (внутренняя подкачка напряжения NOR, самая
|
||||
«тяжёлая» операция по току среди flash-команд), а не логической ошибки на
|
||||
стороне хоста — программно починить плохую пайку нельзя, но можно дать
|
||||
операции второй/третий шанс, если условие временное.
|
||||
|
||||
Фикс: `_run_flash_cmd()` — retry (`CMD_RETRY_ATTEMPTS = 3`,
|
||||
`CMD_RETRY_DELAY_S = 0.5`с) вокруг каждой команды, реально трогающей
|
||||
flash-чип (`configure_memory` в `configure_flexspi`/`write_fcb_auto`,
|
||||
`write_memory` в `write_fcb_explicit`/основной записи HAB-образа,
|
||||
`flash_erase_region`, `flash_erase_all`). Между попытками проверяется
|
||||
`detect_flashloader()` — при реальном обрыве связи retry не имеет смысла,
|
||||
сразу уходит в `ConnectionLostError`, как раньше. Если ошибка
|
||||
детерминированная (плата физически неисправна), retry её не маскирует —
|
||||
после исчерпания попыток `_fail_command()` получает управление с тем же
|
||||
`mboot.status_string`, что и раньше, просто с пометкой «после N попыток».
|
||||
Hardware-тест: retry НЕ помог (все 3 попытки — тот же 20106), гипотеза
|
||||
«маргинальный контакт» опровергнута доп. фактами (MCUBootUtility на тех же
|
||||
платах — 10/10; после одного её касания плата навсегда работает и у нас).
|
||||
Retry оставлен как безвредная страховка.
|
||||
|
||||
**Р15 — найденная причина (QE-бит).** Пересчёт десятичных значений в логах
|
||||
MCUBootUtility инструментом (не «на глаз», как в Р13 — тогда значение было
|
||||
прочитано неверно, что увело диагностику в сторону на три раунда) показал:
|
||||
их option word — `3221225991 = 0xC0000207`, наш был `0xC0000007`. Разница —
|
||||
поле quad_mode_setting (биты [11:8]) = 2: flashloader при `configure-memory`
|
||||
устанавливает **энергонезависимый** QE-бит (Quad Enable, Status Register 2
|
||||
bit 1, формат Winbond) на самом чипе. Часть партий W25Q128 приходит с
|
||||
завода с QE=0 — на них наш вариант оставлял чип в SPI-режиме при LUT,
|
||||
настроенных на quad-команды → 20106 на любой операции. Один прогон
|
||||
MCUBootUtility устанавливал QE=1 навсегда — отсюда «после их утилиты наша
|
||||
начинает работать». Исправлено в `FLEXSPI_OPTION_VALUE` (`flash_backend.py`
|
||||
+ `tools/host/flash_usb.py`), плюс явное обнуление второго option-слова —
|
||||
полное зеркало последовательности NXP. Для чипов с уже установленным QE
|
||||
повторная установка — no-op (статус-регистр, не массив памяти).
|
||||
|
||||
### 8.5 UI (`flash.py`)
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,16 @@ def test_detect_cdc_not_found(monkeypatch):
|
|||
assert fb.detect_cdc() is False
|
||||
|
||||
|
||||
def test_detect_flashloader_found(monkeypatch):
|
||||
monkeypatch.setattr(fb.MbootUSBInterface, "scan", Mock(return_value=[Mock()]))
|
||||
assert fb.detect_flashloader() is True
|
||||
|
||||
|
||||
def test_detect_flashloader_not_found(monkeypatch):
|
||||
monkeypatch.setattr(fb.MbootUSBInterface, "scan", Mock(return_value=[]))
|
||||
assert fb.detect_flashloader() is False
|
||||
|
||||
|
||||
# ─── wait_for_flashloader ────────────────────────────────────────────────
|
||||
|
||||
|
||||
|
|
@ -157,17 +167,30 @@ def test_load_flashloader_happy_path(monkeypatch, events):
|
|||
def test_configure_flexspi_ok():
|
||||
mboot = Mock(configure_memory=Mock(return_value=True))
|
||||
fb.configure_flexspi(mboot)
|
||||
mboot.fill_memory.assert_called_once_with(
|
||||
fb.FLEXSPI_OPTION_ADDR, 4, fb.FLEXSPI_OPTION_VALUE
|
||||
)
|
||||
# Р15: option0 (0xC0000207 — включая QE-бит) + явное обнуление option1,
|
||||
# зеркало последовательности NXP MCUBootUtility.
|
||||
assert mboot.fill_memory.call_args_list == [
|
||||
((fb.FLEXSPI_OPTION_ADDR, 4, fb.FLEXSPI_OPTION_VALUE),),
|
||||
((fb.FLEXSPI_OPTION_ADDR + 4, 4, 0),),
|
||||
]
|
||||
mboot.configure_memory.assert_called_once_with(
|
||||
fb.FLEXSPI_OPTION_ADDR, fb.FLEXSPI_MEMORY_ID
|
||||
)
|
||||
|
||||
|
||||
def test_flexspi_option_value_sets_winbond_qe():
|
||||
"""Р15-регрессия: quad_mode_setting (биты [11:8]) обязан быть 2 —
|
||||
«установить QE-бит в Status Register 2 bit 1» (Winbond W25Q). Со значением
|
||||
0 чипы из партий с заводским QE=0 не работают вообще (20106 на любой
|
||||
операции) — см. модульный docstring, Р15."""
|
||||
assert (fb.FLEXSPI_OPTION_VALUE >> 8) & 0xF == 2
|
||||
assert fb.FLEXSPI_OPTION_VALUE == 0xC0000207
|
||||
|
||||
|
||||
def test_configure_flexspi_fail(monkeypatch):
|
||||
# Плата на месте → честная ошибка операции, НЕ обрыв (вариант B, Фаза 4a).
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=True))
|
||||
monkeypatch.setattr(fb, "detect_flashloader", Mock(return_value=True))
|
||||
monkeypatch.setattr(fb.time, "sleep", Mock()) # не ждать реально между retry (Р14)
|
||||
mboot = Mock(configure_memory=Mock(return_value=False))
|
||||
with pytest.raises(fb.FlashBackendError) as ei:
|
||||
fb.configure_flexspi(mboot)
|
||||
|
|
@ -200,7 +223,8 @@ def test_write_fcb_explicit_ok(tmp_path):
|
|||
|
||||
|
||||
def test_write_fcb_explicit_write_fails(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=True))
|
||||
monkeypatch.setattr(fb, "detect_flashloader", Mock(return_value=True))
|
||||
monkeypatch.setattr(fb.time, "sleep", Mock()) # не ждать реально между retry (Р14)
|
||||
fcb = tmp_path / "w25q128_fdcb.bin"
|
||||
fcb.write_bytes(b"\xab" * 512)
|
||||
mboot = Mock(write_memory=Mock(return_value=False))
|
||||
|
|
@ -321,8 +345,9 @@ def test_flash_write_memory_fails(monkeypatch, events, tmp_path):
|
|||
hab_bin.write_bytes(b"\xd1")
|
||||
|
||||
monkeypatch.setattr(
|
||||
fb, "detect_sdp", Mock(return_value=True)
|
||||
fb, "detect_flashloader", Mock(return_value=True)
|
||||
) # плата на месте → честная ошибка
|
||||
monkeypatch.setattr(fb.time, "sleep", Mock()) # не ждать реально между retry (Р14)
|
||||
monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=Mock()))
|
||||
_mock_mcuboot_ctx(monkeypatch, write_memory=False)
|
||||
|
||||
|
|
@ -367,7 +392,7 @@ def test_erase_chip_happy_path(monkeypatch, events):
|
|||
|
||||
fb.erase_chip(progress_cb=_collector(events))
|
||||
|
||||
assert flashloader_iface.device.timeout == fb.ERASE_ALL_TIMEOUT_MS
|
||||
assert flashloader_iface.device.timeout == fb.MCUBOOT_CMD_TIMEOUT_MS
|
||||
ctx.flash_erase_all.assert_called_once_with(mem_id=fb.FLEXSPI_MEMORY_ID)
|
||||
ctx.reset.assert_called_once_with(reopen=False)
|
||||
assert _phases(events) == ["configure", "erase", "reset", "done"]
|
||||
|
|
@ -375,8 +400,9 @@ def test_erase_chip_happy_path(monkeypatch, events):
|
|||
|
||||
def test_erase_chip_fails(monkeypatch, events):
|
||||
monkeypatch.setattr(
|
||||
fb, "detect_sdp", Mock(return_value=True)
|
||||
fb, "detect_flashloader", Mock(return_value=True)
|
||||
) # плата на месте → честная ошибка
|
||||
monkeypatch.setattr(fb.time, "sleep", Mock()) # не ждать реально между retry (Р14)
|
||||
monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=MagicMock()))
|
||||
_mock_mcuboot_ctx(monkeypatch, flash_erase_all=False)
|
||||
|
||||
|
|
@ -511,25 +537,83 @@ def test_connection_lost_tuple_covers_timeout():
|
|||
assert not issubclass(fb.SPSDKTimeoutError, fb.SPSDKConnectionError)
|
||||
|
||||
|
||||
def test_sdp_still_present_swallows_check_error(monkeypatch):
|
||||
"""§B: ошибка самой проверки detect_sdp() трактуется как «устройства нет»."""
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(side_effect=RuntimeError("bus gone")))
|
||||
assert fb._sdp_still_present() is False
|
||||
def test_flashloader_still_present_swallows_check_error(monkeypatch):
|
||||
"""§B: ошибка самой проверки detect_flashloader() трактуется как «устройства нет»."""
|
||||
monkeypatch.setattr(
|
||||
fb, "detect_flashloader", Mock(side_effect=RuntimeError("bus gone"))
|
||||
)
|
||||
assert fb._flashloader_still_present() is False
|
||||
|
||||
|
||||
def test_fail_command_device_gone_is_connection_lost(monkeypatch):
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=False))
|
||||
monkeypatch.setattr(fb, "detect_flashloader", Mock(return_value=False))
|
||||
mboot = Mock(status_string="NoResponse")
|
||||
with pytest.raises(fb.ConnectionLostError) as ei:
|
||||
fb._fail_command("flash_erase_all вернул False")
|
||||
fb._fail_command(mboot, "flash_erase_all вернул False")
|
||||
assert ei.value.connection_lost is True
|
||||
assert "NoResponse" in str(ei.value)
|
||||
|
||||
|
||||
def test_fail_command_device_present_is_plain_error(monkeypatch):
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=True))
|
||||
monkeypatch.setattr(fb, "detect_flashloader", Mock(return_value=True))
|
||||
mboot = Mock(status_string="kStatus_FlashCommandFailure")
|
||||
with pytest.raises(fb.FlashBackendError) as ei:
|
||||
fb._fail_command("flash_erase_all вернул False")
|
||||
fb._fail_command(mboot, "flash_erase_all вернул False")
|
||||
assert not isinstance(ei.value, fb.ConnectionLostError)
|
||||
assert ei.value.connection_lost is False
|
||||
assert "kStatus_FlashCommandFailure" in str(ei.value)
|
||||
|
||||
|
||||
# ─── Р14: retry маргинального контакта (_run_flash_cmd) ──────────────────
|
||||
|
||||
|
||||
def test_run_flash_cmd_succeeds_first_try(monkeypatch):
|
||||
mboot = Mock(status_string="Success")
|
||||
cmd = Mock(return_value=True)
|
||||
fb._run_flash_cmd(mboot, "test_cmd", cmd)
|
||||
cmd.assert_called_once()
|
||||
|
||||
|
||||
def test_run_flash_cmd_retries_then_succeeds(monkeypatch):
|
||||
monkeypatch.setattr(fb, "detect_flashloader", Mock(return_value=True))
|
||||
monkeypatch.setattr(fb.time, "sleep", Mock())
|
||||
mboot = Mock(status_string="NoResponse")
|
||||
cmd = Mock(side_effect=[False, False, True])
|
||||
|
||||
fb._run_flash_cmd(mboot, "test_cmd", cmd)
|
||||
|
||||
assert cmd.call_count == 3
|
||||
|
||||
|
||||
def test_run_flash_cmd_gives_up_after_max_attempts_device_present(monkeypatch):
|
||||
monkeypatch.setattr(fb, "detect_flashloader", Mock(return_value=True))
|
||||
sleep_mock = Mock()
|
||||
monkeypatch.setattr(fb.time, "sleep", sleep_mock)
|
||||
mboot = Mock(status_string="kStatus_FlashCommandFailure")
|
||||
cmd = Mock(return_value=False)
|
||||
|
||||
with pytest.raises(fb.FlashBackendError) as ei:
|
||||
fb._run_flash_cmd(mboot, "test_cmd", cmd)
|
||||
|
||||
assert not isinstance(ei.value, fb.ConnectionLostError)
|
||||
assert cmd.call_count == fb.CMD_RETRY_ATTEMPTS
|
||||
assert sleep_mock.call_count == fb.CMD_RETRY_ATTEMPTS - 1
|
||||
assert f"после {fb.CMD_RETRY_ATTEMPTS} попыт" in str(ei.value)
|
||||
|
||||
|
||||
def test_run_flash_cmd_bails_immediately_if_device_gone(monkeypatch):
|
||||
"""Обрыв связи между попытками — retry бессмыслен, не тратим время."""
|
||||
monkeypatch.setattr(fb, "detect_flashloader", Mock(return_value=False))
|
||||
sleep_mock = Mock()
|
||||
monkeypatch.setattr(fb.time, "sleep", sleep_mock)
|
||||
mboot = Mock(status_string="NoResponse")
|
||||
cmd = Mock(return_value=False)
|
||||
|
||||
with pytest.raises(fb.ConnectionLostError):
|
||||
fb._run_flash_cmd(mboot, "test_cmd", cmd)
|
||||
|
||||
cmd.assert_called_once()
|
||||
sleep_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_load_flashloader_timeout_is_connection_lost(monkeypatch):
|
||||
|
|
@ -581,19 +665,79 @@ def test_flash_write_false_device_gone_is_connection_lost(
|
|||
|
||||
monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=Mock()))
|
||||
_mock_mcuboot_ctx(monkeypatch, write_memory=False)
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=False))
|
||||
monkeypatch.setattr(fb, "detect_flashloader", Mock(return_value=False))
|
||||
|
||||
with pytest.raises(fb.ConnectionLostError):
|
||||
fb.flash(hab_bin, progress_cb=_collector(events))
|
||||
|
||||
|
||||
def test_erase_chip_false_device_gone_is_connection_lost(monkeypatch, events):
|
||||
"""Гейт 4a #2a: flash_erase_all=False + detect_sdp=False → ConnectionLostError."""
|
||||
"""Гейт 4a #2a: flash_erase_all=False + detect_flashloader=False → ConnectionLostError."""
|
||||
monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=MagicMock()))
|
||||
_mock_mcuboot_ctx(monkeypatch, flash_erase_all=False)
|
||||
monkeypatch.setattr(fb, "detect_sdp", Mock(return_value=False))
|
||||
monkeypatch.setattr(fb, "detect_flashloader", Mock(return_value=False))
|
||||
|
||||
with pytest.raises(fb.ConnectionLostError) as ei:
|
||||
fb.erase_chip(progress_cb=_collector(events))
|
||||
assert ei.value.connection_lost is True
|
||||
assert "done" not in _phases(events)
|
||||
|
||||
|
||||
# ─── Р13: единый таймаут ДО первой Flashloader-команды (не только erase) ──
|
||||
|
||||
|
||||
def test_flash_sets_timeout_before_first_command(monkeypatch, events, tmp_path):
|
||||
"""Полевой баг: flash_erase_region молча падал по spsdk-дефолту 2000мс,
|
||||
т.к. flash() никогда не поднимал iface.device.timeout. Таймаут должен
|
||||
быть выставлен ДО configure_flexspi(), а не только перед erase."""
|
||||
hab_bin = tmp_path / "fw_hab.bin"
|
||||
hab_bin.write_bytes(b"\xd1" + b"\x00" * 15)
|
||||
|
||||
flashloader_iface = Mock()
|
||||
flashloader_iface.device.timeout = 2000 # spsdk-дефолт, UsbDevice.__init__
|
||||
monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=flashloader_iface))
|
||||
ctx = _mock_mcuboot_ctx(monkeypatch)
|
||||
|
||||
seen_timeouts = []
|
||||
ctx.configure_memory.side_effect = (
|
||||
lambda *a, **k: seen_timeouts.append(flashloader_iface.device.timeout) or True
|
||||
)
|
||||
|
||||
fb.flash(hab_bin, progress_cb=_collector(events))
|
||||
|
||||
assert seen_timeouts, "configure_memory ни разу не вызван — тест не проверяет ничего"
|
||||
assert all(t == fb.MCUBOOT_CMD_TIMEOUT_MS for t in seen_timeouts)
|
||||
|
||||
|
||||
def test_erase_chip_sets_timeout_before_configure(monkeypatch, events):
|
||||
"""Тот же баг на шаге раньше: до фикса timeout поднимался ПОСЛЕ
|
||||
configure_flexspi(), значит сам configure всё ещё шёл на 2000мс."""
|
||||
flashloader_iface = MagicMock()
|
||||
flashloader_iface.device.timeout = 2000
|
||||
monkeypatch.setattr(fb, "load_flashloader", Mock(return_value=flashloader_iface))
|
||||
ctx = _mock_mcuboot_ctx(monkeypatch)
|
||||
|
||||
seen_timeouts = []
|
||||
ctx.configure_memory.side_effect = (
|
||||
lambda *a, **k: seen_timeouts.append(flashloader_iface.device.timeout) or True
|
||||
)
|
||||
|
||||
fb.erase_chip(progress_cb=_collector(events))
|
||||
|
||||
assert seen_timeouts, "configure_memory ни разу не вызван — тест не проверяет ничего"
|
||||
assert all(t == fb.MCUBOOT_CMD_TIMEOUT_MS for t in seen_timeouts)
|
||||
|
||||
|
||||
# ─── Р13, ОПРОВЕРГНУТАЯ гипотеза — commit FCB (0xF000000F) ДО erase ───────
|
||||
#
|
||||
# Была здесь как test_flash_commits_fcb_before_erase /
|
||||
# test_erase_chip_commits_fcb_before_erase, пинила write_fcb_auto(mboot)
|
||||
# сразу после configure_flexspi(mboot), до erase (по аналогии с
|
||||
# boot_utility_log.txt — NXP MCUBootUtility на той же плате). Проверено на
|
||||
# живом железе (2026-07-13) и ОПРОВЕРГНУТО: configure-memory(0xF000000F)
|
||||
# физически пишет FCB во flash немедленно, а не просто «донастраивает
|
||||
# контроллер» — на НЕ стёртой области (то есть на любой ранее прошитой
|
||||
# плате) запись сразу проваливается со status 10203 «Memory Cumulative
|
||||
# Write». Сломало ранее рабочую плату. Правка отменена — см. docstring
|
||||
# модуля flash_backend.py, раздел «Р13, ОПРОВЕРГНУТАЯ гипотеза», не
|
||||
# повторять без подтверждения по официальной документации NXP.
|
||||
|
|
|
|||
Loading…
Reference in a new issue