#!/usr/bin/env python3 """ flash_usb.py — прошивка MIMXRT1052 через USB (BootROM SDP → Flashloader → blhost) Использование: python3 flash_usb.py --firmware firmware_test --build-type Debug python3 flash_usb.py --firmware app --build-type Release python3 flash_usb.py --firmware bootloader --build-type Release python3 flash_usb.py --firmware firmware_test --build-type Debug --ram-only python3 flash_usb.py --bin-path /path/to/custom.bin python3 flash_usb.py --erase-chip Шаги (прошивка): 1. Устройство в SDP режиме (VID:PID 1FC9:0130) → sdphost загружает ivt_flashloader.bin в RAM → sdphost прыгает на flashloader 2. Flashloader запущен (VID:PID 15A2:0073) → blhost конфигурирует FlexSPI NOR (пишет FCB) → blhost стирает нужный регион Flash → blhost пишет HAB образ начиная с 0x60001000 → blhost reset Шаги (chip erase): 1. Загрузка Flashloader (аналогично прошивке) 2. blhost configure-memory (инициализация FlexSPI контроллера) 3. blhost flash-erase-all (полная очистка W25Q, ~30 с) 4. blhost reset Конфигурация: Переменные окружения (задаются в .env, экспортируются через just): BOOTROM_VID — VID BootROM SDP (default: 1fc9) BOOTROM_PID — PID BootROM SDP (default: 0130) FLASHLOADER_VID — VID Flashloader (default: 15a2) FLASHLOADER_PID — PID Flashloader (default: 0073) BUILD_DIR — путь к директории сборки (default: /build) """ import argparse import os import subprocess import sys import time from pathlib import Path # ─── Пути ───────────────────────────────────────────────────────────────────── SCRIPT_DIR = Path(__file__).parent.resolve() REPO_ROOT = SCRIPT_DIR.parent.parent FLASHLOADER = SCRIPT_DIR / "dcd" / "ivt_flashloader.bin" BUILD_DIR = Path(os.environ.get("BUILD_DIR", str(REPO_ROOT / "build"))) # ─── USB VID:PID ────────────────────────────────────────────────────────────── def _usb(vid_key: str, vid_default: str, pid_key: str, pid_default: str) -> str: vid = os.environ.get(vid_key, vid_default).strip().upper().lstrip("0X") pid = os.environ.get(pid_key, pid_default).strip().upper().lstrip("0X") return f"0x{vid},0x{pid}" SDP_USB = _usb("BOOTROM_VID", "1fc9", "BOOTROM_PID", "0130") BLHOST_USB = _usb("FLASHLOADER_VID", "15a2", "FLASHLOADER_PID", "0073") # ─── Аппаратные константы ───────────────────────────────────────────────────── # FlexSPI NOR config option word: 0xC0000007 # bits[31:28]=0xC — tag (QuadSPI NOR) # bits[3:0]=0x7 — option size FLEXSPI_OPTION_ADDR = "0x2000" FLEXSPI_OPTION_VALUE = "0xC0000007" FLEXSPI_MEMORY_ID = "9" # FlexSPI NOR memory interface ID # Option word для записи FCB: tag=0xF → Write FCB command FLEXSPI_FCB_VALUE = "0xF000000F" # Flash layout FLASH_BASE = 0x60000000 HAB_OFFSET = 0x1000 # IVT offset: write address = FLASH_BASE + HAB_OFFSET ERASE_ALL_TIMEOUT_MS = "200000" # W25Q512 стирается заметно дольше W25Q128 # ─── Helpers ────────────────────────────────────────────────────────────────── def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess: print(f" $ {' '.join(cmd)}") result = subprocess.run(cmd, capture_output=False) if check and result.returncode != 0: print(f"\n[ERROR] Команда завершилась с кодом {result.returncode}") sys.exit(1) return result def step(msg: str) -> None: print(f"\n{'─' * 60}") print(f" {msg}") print(f"{'─' * 60}") def wait_for_flashloader(timeout: int = 10) -> bool: """Ждём пока Flashloader поднимется (опрашиваем blhost раз в секунду).""" print(f"\n Ожидание Flashloader (до {timeout}с)...", end="", flush=True) for i in range(timeout): time.sleep(1) result = subprocess.run( ["blhost", "-u", BLHOST_USB, "-j", "--", "get-property", "1", "0"], capture_output=True, ) if result.returncode == 0: print(f" OK ({i + 1}с)") return True print(".", end="", flush=True) print(" TIMEOUT") return False def load_flashloader() -> None: """Загружает Flashloader через SDP если ещё не запущен.""" result = subprocess.run( ["blhost", "-u", BLHOST_USB, "-j", "--", "get-property", "1", "0"], capture_output=True, ) if result.returncode == 0: print(" Flashloader уже запущен — пропускаем загрузку") return if not FLASHLOADER.exists(): print(f"[ERROR] Не найден: {FLASHLOADER}", file=sys.stderr) print( " Скачай ivt_flashloader.bin из MCUXpresso Secure Provisioning Tool\n" " и положи в tools/host/dcd/ivt_flashloader.bin", file=sys.stderr, ) sys.exit(1) step("Загрузка Flashloader через SDP (1FC9:0130)") run( [ "sdphost", "-u", SDP_USB, "-j", "--", "write-file", "0x20001C00", str(FLASHLOADER), ] ) run(["sdphost", "-u", SDP_USB, "-j", "--", "jump-address", "0x20001C00"]) if not wait_for_flashloader(): print( "[ERROR] Flashloader не ответил. Проверь BOOT_MOD пины и подключение.", file=sys.stderr, ) sys.exit(1) def configure_flexspi() -> None: """Инициализирует FlexSPI NOR контроллер через Flashloader.""" step("Конфигурация FlexSPI NOR (инициализация контроллера)") run( [ "blhost", "-u", BLHOST_USB, "--", "fill-memory", FLEXSPI_OPTION_ADDR, "4", FLEXSPI_OPTION_VALUE, "word", ] ) run( [ "blhost", "-u", BLHOST_USB, "--", "configure-memory", FLEXSPI_MEMORY_ID, FLEXSPI_OPTION_ADDR, ] ) def write_fcb() -> None: """Записывает Flash Configuration Block в 0x60000000. Отдельный шаг после erase: Flashloader генерирует FCB из параметров FlexSPI и пишет его по адресу 0x60000000. Без FCB BootROM не знает как читать Flash. Option word 0xF000000F: tag=0xF → Write FCB command. """ step("Запись FCB в Flash[0x60000000]") run( [ "blhost", "-u", BLHOST_USB, "--", "fill-memory", FLEXSPI_OPTION_ADDR, "4", FLEXSPI_FCB_VALUE, "word", ] ) run( [ "blhost", "-u", BLHOST_USB, "--", "configure-memory", FLEXSPI_MEMORY_ID, FLEXSPI_OPTION_ADDR, ] ) def write_fcb_explicit(fcb_path: Path) -> None: """Записывает буквальный FCB-блоб (512 байт) в Flash[0x60000000]. В отличие от write_fcb() (magic option word 0xF000000F — auto-config Flashloader, надёжно проверен только для W25Q128), здесь FCB пишется байт-в-байт через write-memory. Нужен для custom-бинарей: nxpimage всегда собирает "чистый" app-образ без FCB (см. hab_*.yaml — FCB туда не входит), поэтому его нужно подставлять явно под конкретный чип — tools/host/dcd/w25q128_fdcb.bin или tools/host/dcd/w25q512_fdcb.bin. """ if not fcb_path.exists(): print(f"[ERROR] FCB-файл не найден: {fcb_path}", file=sys.stderr) sys.exit(1) step(f"Запись явного FCB ({fcb_path.name}) в Flash[0x60000000]") run( [ "blhost", "-u", BLHOST_USB, "--", "write-memory", f"0x{FLASH_BASE:08X}", str(fcb_path), "0", ] ) # ─── Основные операции ──────────────────────────────────────────────────────── def flash(hab_bin: Path, ram_only: bool = False, fcb_path: Path | None = None) -> None: """Прошить HAB-образ в Flash или загрузить в RAM.""" if not hab_bin.exists(): print(f"[ERROR] Файл не найден: {hab_bin}", file=sys.stderr) print( " Сначала собери образ: uv run nxpimage hab export -c ...", file=sys.stderr ) sys.exit(1) if ram_only: step(f"Загрузка в RAM (без записи во Flash): {hab_bin.name}") run( [ "sdphost", "-u", SDP_USB, "-j", "--", "write-file", f"0x{FLASH_BASE + HAB_OFFSET:08X}", str(hab_bin), ] ) run( [ "sdphost", "-u", SDP_USB, "-j", "--", "jump-address", f"0x{FLASH_BASE + HAB_OFFSET:08X}", ] ) return load_flashloader() configure_flexspi() write_addr = f"0x{FLASH_BASE + HAB_OFFSET:08X}" erase_size = ((HAB_OFFSET + hab_bin.stat().st_size + 0xFFF) // 0x1000) * 0x1000 step(f"Прошивка Flash: {hab_bin.name}") print(f" Образ: {hab_bin}") print(f" Размер: {hab_bin.stat().st_size} байт") print(f" Адрес: {write_addr}") print(f" Стирание: 0x{FLASH_BASE:08X} .. +{erase_size} байт") run( [ "blhost", "-u", BLHOST_USB, "--", "flash-erase-region", f"0x{FLASH_BASE:08X}", str(erase_size), "0", ] ) if fcb_path is not None: write_fcb_explicit(fcb_path) else: write_fcb() run( [ "blhost", "-u", BLHOST_USB, "--", "write-memory", write_addr, str(hab_bin), "0", ] ) step("Reset") run(["blhost", "-u", BLHOST_USB, "--", "reset"]) print("\n ✅ Прошивка завершена успешно") def erase_chip() -> None: """Полная очистка Flash (chip erase) через Flashloader. Использует blhost flash-erase-all (memory ID 9 = FlexSPI NOR). Время операции: ~30 с для W25Q128. После erase FCB также стёрт — BootROM не сможет загрузить прошивку до следующей прошивки (flash_usb.py запишет FCB автоматически). """ load_flashloader() configure_flexspi() step("Полная очистка Flash (chip erase, ~30 с)") print(" ⚠️ После chip erase BootROM не сможет загрузить прошивку.") print(" ⚠️ Используй flash_usb.py для восстановления.\n") run( [ "blhost", "-t", ERASE_ALL_TIMEOUT_MS, "-u", BLHOST_USB, "--", "flash-erase-all", FLEXSPI_MEMORY_ID, ] ) step("Reset") run(["blhost", "-u", BLHOST_USB, "--", "reset"]) print("\n ✅ Chip erase завершён") # ─── CLI ────────────────────────────────────────────────────────────────────── def main() -> None: parser = argparse.ArgumentParser( description="Прошивка/очистка MIMXRT1052 через USB SDP", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) parser.add_argument( "--fcb-path", type=Path, metavar="PATH", default=None, help=( "Явный FCB-блоб (512 байт) для записи в 0x60000000 вместо " "auto-config Flashloader. Имеет смысл только с --bin-path." ), ) # Группа: что прошивать (взаимоисключающие варианты) target_group = parser.add_mutually_exclusive_group() target_group.add_argument( "--firmware", choices=["firmware_test", "bootloader", "app"], help="Стандартная прошивка из BUILD_DIR (требует --build-type)", ) target_group.add_argument( "--bin-path", type=Path, metavar="PATH", help="Путь к произвольному HAB-бинарю (.bin) для прошивки", ) target_group.add_argument( "--erase-chip", action="store_true", help="Полная очистка Flash (chip erase, ~30 с). Прошивка не выполняется.", ) parser.add_argument( "--build-type", choices=["Debug", "Release"], default="Release", help="Тип сборки (только для --firmware, default: Release)", ) parser.add_argument( "--ram-only", action="store_true", help="Загрузить в RAM без записи во Flash (только для --firmware / --bin-path)", ) args = parser.parse_args() # Валидация: --firmware требует --build-type (уже есть default, но запомним) # --bin-path: build-type игнорируется # --erase-chip: несовместим с --ram-only if args.erase_chip and args.ram_only: parser.error("--erase-chip несовместим с --ram-only") if args.fcb_path is not None and args.bin_path is None: parser.error("--fcb-path имеет смысл только вместе с --bin-path") if args.firmware is None and args.bin_path is None and not args.erase_chip: parser.error("Укажи --firmware, --bin-path или --erase-chip") # ── Определить hab_bin ────────────────────────────────────────────────── hab_bin: Path | None = None if args.firmware is not None: hab_bin = BUILD_DIR / args.build_type / f"{args.firmware}_hab.bin" elif args.bin_path is not None: hab_bin = args.bin_path.resolve() # ── Шапка ────────────────────────────────────────────────────────────── print(f"\n{'═' * 60}") print(" MIMXRT1052 Flash Tool (USB SDP)") if args.erase_chip: print(" Операция: chip erase") elif hab_bin is not None: label = ( f"{args.firmware} [{args.build_type}]" if args.firmware else str(hab_bin) ) print(f" Прошивка: {label}") print(f" Образ: {hab_bin}") if args.ram_only: print(" Режим: RAM only (без записи во Flash)") print(f" SDP USB: {SDP_USB}") print(f" BL USB: {BLHOST_USB}") print(f"{'═' * 60}\n") # ── Выполнить операцию ────────────────────────────────────────────────── if args.erase_chip: erase_chip() elif hab_bin is not None: flash(hab_bin, ram_only=args.ram_only, fcb_path=args.fcb_path) if __name__ == "__main__": main()