From d057df7698b4372e43df1da6d7d807947f0ee28e Mon Sep 17 00:00:00 2001 From: Andrey Date: Fri, 13 Nov 2026 17:02:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=BF=D1=80=D0=BE=D1=88=D0=B8=D0=B2=D0=BA=D1=83=20F28?= =?UTF-8?q?335=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20SCI=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=D0=B3=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D1=83=20Python?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Подключить сборку Flash BIN и RAM-ядра TI через templates. Передавать настройки CAN и RS из JSON и проверять окружение CCS/Keil. Добавить общий C99-протокол и проверки обмена SCI. --- .../tests/test_flashing_settings.py | 48 +++++++ c/firmware-info/tools/c2000_build.py | 9 ++ c/firmware-info/tools/c2000_sci_build.py | 101 +++++++++++++ .../tools/check_build_environment.py | 13 ++ c/set-protocol/CMakeLists.txt | 4 + c/set-protocol/include/c2000_sci.h | 18 +++ c/set-protocol/src/c2000_sci.c | 106 ++++++++++++++ c/set-protocol/tests/test_c2000_sci.c | 37 +++++ c/set-protocol/tools/build_host.py | 1 + python/protocan/c2000_sci.py | 63 +++++++++ .../set_devices/qt_ports/c2000_sci_panel.py | 133 ++++++++++++++++++ python/tests/test_c2000_sci.py | 51 +++++++ tools/firmware-build/README.md | 126 +++++++++++++++++ .../ccs/firmware_identity.example.json | 33 ++++- tools/firmware-build/ccs/postcompile.bat | 6 +- tools/firmware-build/ccs/precompile.bat | 3 +- .../keil/firmware_identity.example.json | 19 ++- tools/firmware-build/keil/postcompile.bat | 12 +- tools/firmware-build/keil/precompile.bat | 3 +- tools/firmware-build/python-runtime.ps1 | 87 ++++++++++++ 20 files changed, 863 insertions(+), 10 deletions(-) create mode 100644 c/firmware-info/tests/test_flashing_settings.py create mode 100644 c/firmware-info/tools/c2000_sci_build.py create mode 100644 c/firmware-info/tools/check_build_environment.py create mode 100644 c/set-protocol/include/c2000_sci.h create mode 100644 c/set-protocol/src/c2000_sci.c create mode 100644 c/set-protocol/tests/test_c2000_sci.c create mode 100644 python/protocan/c2000_sci.py create mode 100644 python/set_devices/qt_ports/c2000_sci_panel.py create mode 100644 python/tests/test_c2000_sci.py create mode 100644 tools/firmware-build/python-runtime.ps1 diff --git a/c/firmware-info/tests/test_flashing_settings.py b/c/firmware-info/tests/test_flashing_settings.py new file mode 100644 index 0000000..91905b6 --- /dev/null +++ b/c/firmware-info/tests/test_flashing_settings.py @@ -0,0 +1,48 @@ +"""Профили CAN/RS проходят из JSON Keil/CCS до проверенного manifest и архива.""" # Проверки не требуют IDE, GUI, подключённой платы или сети. +import importlib.util # Загружает действующий общий генератор независимо от установленного Python-пакета. +import json # Проверяет сохранённые JSON, а не только возвращаемые словари. +from pathlib import Path # Все временные файлы отделены от настоящих проектов прошивок. +import tempfile # Изолирует счётчик версии и архив каждого сценария. +import unittest # Запускает проверки стандартной библиотекой Python. +from unittest.mock import patch # Фиксирует происхождение сборки без изменения настоящего Git. + +ROOT = Path(__file__).resolve().parents[3] # Корень templates находится выше c/firmware-info/tests. +SPEC = importlib.util.spec_from_file_location("flashing_identity_tests", ROOT / "c/firmware-info/tools/identity.py") # Выбирает общий генератор, используемый Keil и CCS. +IDENTITY = importlib.util.module_from_spec(SPEC) # Создаёт изолированный модуль для проверки полного цикла выпуска. +SPEC.loader.exec_module(IDENTITY) # Импорт не запускает CLI или резервирование версии. + + +class FlashingSettingsTests(unittest.TestCase): # Проверяет перенос профилей и ранний отказ при ошибках настройки. + def test_keil_and_ccs_profiles_survive_seal_archive_and_export(self): # Все параметры должны сохраниться до файла, который читает Публикатор SETGUI. + for ide in ("keil", "ccs"): # Проверяет реальные примеры обеих IDE одним и тем же полным циклом. + with self.subTest(ide=ide), tempfile.TemporaryDirectory() as temporary: # Каждый проект получает независимые файлы и версию. + root = Path(temporary) # Тест никогда не меняет пользовательский firmware_identity.json. + settings = json.loads((ROOT / f"tools/firmware-build/{ide}/firmware_identity.example.json").read_text(encoding="utf-8")) # Реальные примеры также обязаны соответствовать контракту. + config = root / "config.json" # Исходные настройки отделены от сгенерированного паспорта. + config.write_text(json.dumps(settings), encoding="utf-8") # Сохраняет рабочий JSON в требуемой кодировке. + with patch.object(IDENTITY, "git_state", return_value={"commit": "a" * 40, "dirty": False}): # Происхождение воспроизводимо без коммитов в настоящем проекте. + metadata = IDENTITY.prepare(root, ROOT, config, root / "generated") # Выполняет настоящий prebuild с резервированием версии. + self.assertEqual(metadata["flashing"], settings["flashing"]) # Ни скорости, ни альтернативные маршруты не теряются при генерации. + self.assertEqual(json.loads(config.read_text(encoding="utf-8"))["version"], [0, 1, 1]) # Параметры не ломают существующий итератор prebuild. + image = root / "firmware.bin" # Малый файл проверяет только доставку настроек, не MCU-кодек. + image.write_bytes(b"exact firmware bytes") # SHA-256 должен связывать настройки именно с этими байтами. + metadata_path = root / "generated/firmware_identity.json" # Postbuild читает только зафиксированный паспорт prebuild. + IDENTITY.seal(image, metadata_path) # Записывает manifest и SHA-256 штатным общим механизмом. + result = IDENTITY.archive_build(image, metadata_path, root / "archive", export=root / "setgui") # Проходит локальный архив и полную экспортную копию. + manifest = Path(result["exported_directory"]) / "firmware.bin.manifest.json" # Читает точный sidecar, доступный другому клиенту SETGUI. + self.assertEqual(IDENTITY.verify(manifest.with_name("firmware.bin"), manifest)["flashing"], settings["flashing"]) # Проверяет целостность экспортированного файла и сохранность профилей. + + def test_invalid_profiles_do_not_reserve_version(self): # Опечатка скорости не должна увеличивать номер не начавшейся сборки. + invalid = ({"profiles": {"set-can": {"bitrate": True}}}, {"profiles": {"set-can": {"bitrate": 1000001}}}, {"profiles": {"set-rs485": {"baudrate": "115200"}}}, {"profiles": {"protocan-can": {"node_id": 16}}}, {"profiles": {"set-can": {"hardware_min": 2, "hardware_max": 1}}}, {"profiles": {"set-can": {"bitrtae": 250000}}}, {"default_loader": "set-can", "profiles": {}}, None) # Включает типы, предел Classic CAN, адрес ProtoCAN, диапазон ревизий и ошибочные ссылки. + with tempfile.TemporaryDirectory() as temporary: # Все ошибочные настройки остаются в отдельной тестовой папке. + root = Path(temporary) # Один путь позволяет сравнить байты до и после каждого отказа. + for profile in invalid: # Каждая ошибка должна остановить тот же публичный prebuild. + with self.subTest(profile=profile): # Диагностика указывает конкретный ошибочный профиль. + config = root / "config.json" # Не использует настоящую конфигурацию Keil или CCS. + config.write_text(json.dumps({"product": "test", "target": "host", "version": [1, 2, 3], "increment_version": True, "flashing": profile}), encoding="utf-8") # Включённый итератор выявляет слишком позднюю проверку настроек. + before = config.read_bytes() # Эталон включает версию и все поля исходного JSON. + with self.assertRaises(ValueError): IDENTITY.prepare(root, ROOT, config, root / "generated") # Ошибка должна возникнуть до записи новой версии и заголовка. + self.assertEqual(config.read_bytes(), before) # Отказ не изменяет сохраняемый номер или настройки пользователя. + + +if __name__ == "__main__": unittest.main() # Прямой запуск использует те же проверки, что unittest discovery. diff --git a/c/firmware-info/tools/c2000_build.py b/c/firmware-info/tools/c2000_build.py index a7be901..e6f24ae 100644 --- a/c/firmware-info/tools/c2000_build.py +++ b/c/firmware-info/tools/c2000_build.py @@ -15,6 +15,7 @@ import xml.etree.ElementTree as ET # Проверяет фактические import sys # Запускает общий CLI и подключает конвертор без зависимости от GUI. sys.path.insert(0, str(Path(__file__).resolve().parents[3] / 'python')) # Контракт защищённой Flash берётся из того же checkout templates, что общий сборщик. from protocan.f28335_flash import APP_START, APP_DATA, APP_STOP, coff_sections, validate_words # Проверяет отсутствие сектора A и описание FAB1 одинаково для COFF и SCI8. +from set_devices.firmware_flashing import validate_flashing # Сверяет параметры CAN/RS тем же контрактом, что prebuild Keil/CCS и SETGUI. def numeric_define(header, name): # Читает единственное числовое C-определение без исполнения выражений из заголовка платы. text = header.read_text(encoding="utf-8-sig") # Поддерживает UTF-8 с BOM для пользовательских заголовков конфигурации. @@ -363,9 +364,17 @@ def publish(project, templates, tools_templates, compiler, build_dir, artifact, metadata = json.loads(metadata_path.read_text(encoding="utf-8")) # Отсутствующий prebuild останавливает работу до создания выпуска. profile = read_profile(project, templates, eeprom) # Повторно читает текущий выбор EEPROM и общий резерв параметров. settings = json.loads((project / "firmware_identity.json").read_text(encoding="utf-8-sig")) if (project / "firmware_identity.json").is_file() else metadata # Сверяет product и target с действующим проектом. + if validate_flashing(settings.get("flashing", {})) != metadata.get("flashing", {}): raise ValueError("Flashing settings changed after prebuild; rebuild the project") # Изменённые скорость или адрес после компиляции требуют нового prebuild и согласованного manifest. expected = {"product": settings["product"], "target": settings["target"], "version": configured_version(project, settings), **({"templates_root": str(templates)} if "eeprom_bytes" in profile else {}), **profile} # Ручной номер и аппаратный вариант обязаны совпадать с prebuild. if any(metadata.get(key) != value for key, value in expected.items()) or metadata.get("can_boot_profile") != profile.get("can_boot_profile"): # Включение и отключение CAN после prebuild также требуют пересборки, как изменение версии, EEPROM или зависимости. raise ValueError("Configuration changed: run prebuild and rebuild before publishing") # Требует пересборку вместо подмены старых метаданных. + if metadata.get("flashing", {}).get("default_loader") == "f28335-sci": # Настроенный SCI-профиль автоматически подключает общую сборку приложения и RAM-ядра без проектного Python-файла. + if metadata["target"].upper() != "TMS320F28335": raise ValueError("f28335-sci требует target TMS320F28335") # Не применяет Flash API другого процессора по совпадению транспорта. + from types import SimpleNamespace # Передаёт общие проверки в специализированную ветку без копирования их реализации. + spec = importlib.util.spec_from_file_location("shared_c2000_sci_build", tools_templates / "c/firmware-info/tools/c2000_sci_build.py") # Загружает реализацию именно выбранной полной библиотеки templates. + sci = importlib.util.module_from_spec(spec); spec.loader.exec_module(sci) # Импорт не запускает компилятор или запись аппаратуры. + api = SimpleNamespace(load_identity=load_identity, verify_embedded=verify_embedded, verify_package=verify_package, write_receipt=write_receipt) # Один набор проверок обслуживает прежний и новый маршруты выпуска. + return sci.publish(api, project, compiler, build_dir, artifact, metadata, output, export) # Имя *_flash_rs.bin теперь определяется фактической веткой общего postbuild. verify_linker_profile(map_path.resolve(), profile) if "eeprom_bytes" in profile else None # Проверяет аппаратный выбор линкера, который нельзя вывести только из Python-конфигурации. with tempfile.TemporaryDirectory(prefix=".verified-release-", dir=build_dir) as directory: # Единственное место, куда общий postcompile пишет до подтверждения пригодности BIN. stage = Path(directory) # Изолирует все архивы и промежуточные HEX от постоянных каталогов выпуска. diff --git a/c/firmware-info/tools/c2000_sci_build.py b/c/firmware-info/tools/c2000_sci_build.py new file mode 100644 index 0000000..c5bd816 --- /dev/null +++ b/c/firmware-info/tools/c2000_sci_build.py @@ -0,0 +1,101 @@ +"""Общий выпуск Flash SCI8 и RAM-ядра TI для штатного SCI-загрузчика F28335.""" # Подключается автоматически по flashing.default_loader=f28335-sci без проектного build_flash.py. +import base64 # Получает точные данные LOAD-секций из официального XML TI OFD. +import hashlib # Привязывает RAM-ядро к manifest конкретного комплекта прошивки. +import json # Сохраняет тот же UTF-8 паспорт версии и параметры загрузки из проектного JSON. +import os # Учитывает явный путь C2000WARE_ROOT без зависимости от компьютера разработчика. +from pathlib import Path # Все промежуточные файлы находятся внутри явно выбранного staging. +import re # Изменяет только закреплённые настройки официального TI kernel в временной копии. +import shutil # Копирует исходники ядра с сохранением лицензии TI и текущие артефакты проекта. +import subprocess # Запускает локальный TI compiler/hex2000 без JTAG и без аппаратного программирования. +import tempfile # Постоянный выпуск появляется после проверки всех результатов текущей сборки. +import xml.etree.ElementTree as ET # Читает фактические адреса COFF и ссылки исходников официального проекта ядра. + + +def find_c2000ware(): # Автоматически находит установленный комплект TI до компиляции ядра. + candidates = [Path(os.environ[name]) for name in ("C2000WARE_ROOT", "C2000WARE_INSTALL_DIR") if os.environ.get(name)] # Явная установка имеет приоритет над стандартными путями Windows. + candidates += sorted(Path("C:/ti").glob("C2000Ware_*"), reverse=True) # Типовая установка TI не требует ручного добавления каталогов в PATH. + for root in candidates: # Проверяет наличие именно F28335 SCI Flash Kernel A. + if (root / "device_support/f2833x/examples/f28335_flash_kernel/Shared_Boot.c").is_file(): return root.resolve() # Не принимает другой SDK за готовую зависимость. + raise ValueError("Не найден C2000Ware с f28335_flash_kernel; установите C2000Ware или задайте C2000WARE_ROOT") # Автонастройка должна показывать отсутствующую зависимость до сборки проекта. + + +def inspect_flash(image, compiler): # Проверяет обычный Flash OUT UlitCo независимо от названия выходной папки. + document = ET.fromstring(subprocess.check_output([str(compiler / "bin/ofd2000.exe"), "-x", str(image)])) # XML создаёт тот же официальный toolchain, который выпустил COFF. + coff = document.find(".//ti_coff") # SCI-профиль предназначен для исполняемого COFF TMS320F28335. + if coff is None or coff.findtext("file_header/target_id") != "0x9d" or coff.findtext("file_header/exec") != "true": raise ValueError("Для F28335 SCI нужен исполняемый COFF OUT") # Чужой процессор или объектный файл не конвертируется в прошивку. + entry, sections, mask = int(coff.findtext("optional_file_header/entry_point", "0"), 0), [], 0 # Entry и диапазоны OFD выражены в 16-битных словах C28x. + for section in coff.findall("section"): # Учитывает только реально загружаемые секции, исключая DSECT/NOLOAD и отладочные данные. + raw = section.findtext("raw_data") # OFD публикует содержимое загружаемой секции как base64. + if raw is None or section.findtext("copy") == "true": continue # Отсутствующие данные не должны породить ложную запись в ROM или регистры. + data, start = base64.b64decode(raw), int(section.findtext("virtual_addr", "0"), 0) # Использует LOAD-адрес, даже когда код исполняется из RAM. + stop = start + len(data) // 2 # Конец секции исключается; длина файла задана физическими октетами. + if not data: continue # Пустые служебные секции не влияют на Flash-маску. + if len(data) % 2 or start < 0x300000 or stop > 0x33FFF8 or (start < 0x33FFF6 and stop > 0x33FF80): raise ValueError("Flash OUT содержит недопустимую LOAD-секцию: " + str(section.findtext("name"))) # Не допускает RAM-образ, OTP или CSM; аппаратный codestart остаётся разрешён. + if any(start < old_stop and old_start < stop for old_start, old_stop in sections): raise ValueError("Пересекающиеся LOAD-секции Flash OUT") # Одинаковый адрес не может программироваться дважды разными секциями. + sections.append((start, stop)) # Сохраняет диапазон для проверки entry и последующих секций. + for sector in range((0x33FFFF - (stop - 1)) // 0x8000, (0x33FFFF - start) // 0x8000 + 1): mask |= 1 << sector # Маска TI нумерует сектор A младшим битом; сектор содержит 0x8000 слов. + if not sections or not any(start <= entry < stop for start, stop in sections): raise ValueError("Точка входа Flash OUT отсутствует в загружаемых данных") # Наличие OUT без реального кода не разрешает выпуск. + return mask # Ядро стирает только сектора, необходимые этому образу. + + +def build_kernel(compiler, output, oscillator_hz, sectors, ware=None): # Собирает официальный TI Kernel A под частоту платы и маску текущего приложения. + ware = Path(ware) if ware else find_c2000ware() # Применение настроек и postbuild используют одну процедуру поиска зависимости. + device = ware / "device_support/f2833x" # Общие заголовки и периферийные символы берутся из выбранного SDK. + original = device / "examples/f28335_flash_kernel" # Использует штатные исходники TI вместо закрытого ядра C2Prog. + output = Path(output); output.parent.mkdir(parents=True, exist_ok=True) # Ядро создаётся только внутри staging текущего выпуска. + source = output.parent / "kernel-source"; shutil.copytree(original, source) # Сохраняет полный текст лицензии каждого исходника в временной копии. + shared = source / "Shared_Boot.c"; text = shared.read_text(encoding="utf-8-sig") # Меняет только маску erase, не алгоритм Flash API. + text, replaced = re.subn(r"Flash_Erase\(\s*\([^)]*\)", f"Flash_Erase((Uint16)0x{sectors:04X}", text, count=1) # Маска соответствует фактическим Flash LOAD-секциям текущего OUT. + if replaced != 1: raise ValueError("Неподдержанная версия TI Kernel A: не найдена маска Flash_Erase") # Изменённый SDK не должен незаметно стирать другой набор секторов. + shared.write_text(text, encoding="utf-8") # Временный исходник остаётся UTF-8 с сохранённой лицензией TI. + cpu_rate = 1000000000 / (oscillator_hz * 5) # Штатный PLLCR=10 и DIVSEL=/2 дают SYSCLKOUT=5×частота кварца; CPU_RATE выражен наносекундами. + config = source / "Flash2833x_API_Config.h"; text = config.read_text(encoding="utf-8-sig") # Масштабирование Flash API должно соответствовать реальной частоте CPU. + text, replaced = re.subn(r"(?m)^#define\s+CPU_RATE\s+[^\r\n]+", f"#define CPU_RATE {cpu_rate:.6f}L /* Период SYSCLKOUT в наносекундах для выбранного кварца. */", text, count=1) # Не меняет закомментированные примеры частот TI. + if replaced != 1: raise ValueError("В TI kernel отсутствует настройка CPU_RATE") # Отсутствующий масштаб времени не допускает вызов Flash API. + config.write_text(text, encoding="utf-8") # Подготовленный заголовок действует только на RAM-ядро этой сборки. + sources = list(source.glob("*.c")) + list(source.glob("*.asm")); commands = [] # Локальные исходники штатного проекта компилируются без рекурсивного запуска CCS Make. + project = ET.parse(original / ".project") # Связанные исходники SDK разрешаются по стандартным ссылкам проекта TI. + for link in project.findall(".//linkedResources/link"): # Подключает common и headers, не копируя их в проект пользователя. + location = link.findtext("locationURI", "") # Официальный проект использует INSTALLROOT_2833X. + if location.startswith("INSTALLROOT_2833X/"): commands.append(device / location.split("/", 1)[1]) # Сохраняет пути выбранного SDK вместо абсолютных путей TI-разработчика. + sources += [path for path in commands if path.suffix.lower() in (".c", ".asm")] # Включает ADC calibration, delay и периферийные символы F28335. + compiler_args = [str(compiler / "bin/cl2000.exe"), "-v28", "-ml", "-mt", "--float_support=fpu32", "--abi=coffabi", "--define=LARGE_MODEL", "--opt_level=2"] # Параметры соответствуют официальному проекту TI F28335 Kernel A. + compiler_args += ["--include_path=" + str(path) for path in (source, device / "common/include", device / "headers/include", compiler / "include")] # Только заголовки выбранных compiler и SDK участвуют в сборке. + objects = [] # Каждый исходник создаёт свой новый OBJ в staging. + for index, path in enumerate(sources): # Уникальные имена защищают от одноимённых файлов разных каталогов SDK. + obj = source / (str(index) + ".obj"); subprocess.run(compiler_args + ["--compile_only", "--output_file=" + str(obj), str(path)], check=True, cwd=source) # Не использует устаревшие OBJ из Debug официального примера. + objects.append(str(obj)) # Линкер получает только результаты текущего вызова. + out = output.with_suffix(".out") # RAM COFF остаётся промежуточным результатом подготовки ядра. + link_args = [str(compiler / "bin/cl2000.exe"), "-v28", "-ml", "--float_support=fpu32", "--abi=coffabi", "-z", "--rom_model", "--entry_point=_InitBoot", "--stack_size=0x200", "--output_file=" + str(out), "--map_file=" + str(out.with_suffix(".map"))] # Entry _InitBoot и стек повторяют официальную RAM-конфигурацию ядра. + link_args += ["--search_path=" + str(compiler / "lib"), "--search_path=" + str(source)] + objects + [str(path) for path in commands if path.suffix.lower() == ".cmd"] + [str(source / "Flash28335_API_V210.lib"), "-lrts2800_fpu32.lib"] # Flash API берётся из лицензированного SDK на машине пользователя. + subprocess.run(link_args, check=True, cwd=source) # Неполная RAM-компоновка не допускается к упаковке. + subprocess.run([str(compiler / "bin/hex2000.exe"), str(out), "-boot", "-sci8", "-b", "-o", str(output)], check=True) # Получает бинарный SCI8, который первичный загрузчик UlitCo принимает в RAM. + if not output.is_file() or not output.stat().st_size: raise ValueError("TI hex2000 не создал RAM-ядро SCI") # Ложный успех процесса не оставляет неполный комплект. + return output # Ядро проверяется host C-ядром до первого байта COM-передачи. + + +def publish(build, project, compiler, build_dir, artifact, metadata, output, export=None): # Выпускает комплект штатного SCI-профиля из уже собранного Flash OUT. + identity = build.load_identity(Path(__file__).resolve().parents[3]) # Паспорт, архивирование и SHA-256 остаются общими для CCS и Keil. + base = Path(artifact.replace("\\", "/")).name; source = Path(str(Path(artifact) if Path(artifact).is_absolute() else build_dir / artifact) + ".out") # Выбирает точный артефакт текущей конфигурации CCS. + settings = metadata["flashing"]["profiles"]["f28335-sci"] # Конкретный профиль задаёт частоту кварца и параметры UART пользователя. + sectors = inspect_flash(source, compiler) # RAM OUT или файл с защищёнными адресами отвергается до постоянного экспорта. + output = output.resolve(); output.mkdir(parents=True, exist_ok=True) # Каталог выпуска не зависит от дополнительного пути SETGUI. + with tempfile.TemporaryDirectory(prefix=".sci-release-", dir=output) as temporary: # Все преобразования проверяются до изменения постоянного комплекта. + stage = Path(temporary); image = stage / (base + "_flash_rs.bin"); kernel = stage / "f28335_sci_kernel.bin" # Отдельные имена однозначно различают Flash-приложение и RAM-ядро. + build_kernel(compiler, kernel, settings.get("oscillator_hz", 30000000), sectors) # Подготовка Flash больше не требует проектного build_flash.py. + subprocess.run([str(compiler / "bin/hex2000.exe"), str(source), "-boot", "-sci8", "-b", "-map", str(image.with_suffix(".bin.map")), "-o", str(image)], check=True) # BIN содержит LOAD-адреса исходной Flash-компоновки, без переименования RAM-образа. + build.verify_embedded(image, metadata) # Подтверждает полноту SCI8 и встроенный паспорт именно текущего prebuild. + kernel_info = {"file": kernel.name, "size_bytes": kernel.stat().st_size, "sha256": hashlib.sha256(kernel.read_bytes()).hexdigest(), "oscillator_hz": settings.get("oscillator_hz", 30000000), "sector_mask": sectors, "protocol": "TI Flash Kernel A"} # GUI проверяет точный RAM-бинарник перед отправкой первичному загрузчику. + specific = dict(metadata, execution="FLASH", image_format="SCI8-FLASH-SCI", protocol="TI SCI Flash Kernel A", transport="tms", flash_kernel=kernel_info) # Паспорт не объявляет совместимость с отдельным BIOS ПМ35 и FAB1. + passport = stage / "firmware_identity.json"; passport.write_text(json.dumps(specific, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") # Сохраняет все исходные настройки, версию и точное назначение комплекта. + identity.seal(image, passport); build.verify_package(image, specific) # Подтверждает SHA-256 транспортного Flash BIN до архивирования. + coff = stage / (base + "_flash.out"); shutil.copy2(source, coff) # Исходный COFF сохраняется для JTAG и диагностики с точными исходными байтами. + notices = stage / "TI_F28335_kernel_license.json"; notices.write_text(json.dumps({"source": "TI C2000Ware f28335_flash_kernel", "license_notice": "\n".join((find_c2000ware() / "device_support/f2833x/examples/f28335_flash_kernel/main.c").read_text(encoding="utf-8-sig").splitlines()[:49])}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") # Скомпилированное RAM-ядро распространяется с исходным уведомлением и условиями лицензии TI. + extras = [kernel, coff, image.with_suffix(".bin.map"), notices] # Полный комплект включает зависимое ядро и обязательное уведомление поставщика. + result = identity.archive_build(image, passport, stage / "archive", extra_files=extras) # Использует общий проверенный архиватор вместо проектного копирования файлов. + folder = Path(result["directory"]); destination = output / folder.name # Имя выпуска определяется коммитом и временем prebuild в UTC+3. + identity.copy_tree(folder, destination) # Переносит только подготовленный текущий комплект; старые выпуски не сканируются. + if export is not None: identity.export_build(destination, export.resolve(), image.name) # Дополнительная папка SETGUI получает тот же полный проверенный набор. + build.write_receipt(output, destination.name, image.name) # Указатель импорта ведёт прямо к *_flash_rs.bin.manifest.json этого профиля. + print("[firmware-build] Flash SCI BIN: " + str(destination / image.name)) # IDE показывает фактический файл результата вместо неподходящей папки eeprom. + return destination / image.name # Вызывающий может открыть manifest без поиска по времени изменения. diff --git a/c/firmware-info/tools/check_build_environment.py b/c/firmware-info/tools/check_build_environment.py new file mode 100644 index 0000000..4af8fe5 --- /dev/null +++ b/c/firmware-info/tools/check_build_environment.py @@ -0,0 +1,13 @@ +"""Проверка зависимостей автоматической настройки CCS/Keil до изменения проекта.""" # Запускается общей Python-обвязкой из SETGUI без компиляции или обращения к MCU. +import argparse # Принимает только конкретный профиль загрузки из проверенного JSON. +import sys # Возвращает диагностический код установщику проекта. +from c2000_sci_build import find_c2000ware # Поиск F28335 Kernel A совпадает с настоящим postbuild. + +parser = argparse.ArgumentParser(description=__doc__) # Локальная проверка не зависит от запущенной IDE. +parser.add_argument("--loader", default="") # Старые проекты без выбранного SCI-профиля требуют только стандартную библиотеку Python. +args = parser.parse_args() # Не допускает неизвестные параметры проверки окружения. +try: # Отсутствующая зависимость должна остановить применение до создания резервных копий проекта. + if args.loader == "f28335-sci": print("C2000Ware: " + str(find_c2000ware())) # Для родного SCI автоматически проверяет установленное ядро TI. + print("Python: " + sys.executable) # Диагностика показывает фактический интерпретатор prebuild/postbuild. +except (ValueError, OSError) as error: # Ошибки поиска SDK отображаются без Python traceback в интерфейсе оператора. + print(str(error), file=sys.stderr); sys.exit(2) # Ненулевой код не позволяет GUI записать неполную настройку IDE. diff --git a/c/set-protocol/CMakeLists.txt b/c/set-protocol/CMakeLists.txt index e645047..7fb132e 100644 --- a/c/set-protocol/CMakeLists.txt +++ b/c/set-protocol/CMakeLists.txt @@ -10,6 +10,7 @@ set(CMAKE_C_STANDARD_REQUIRED ON) # SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow. set(SETPROTOCOL_V2_SOURCES + src/c2000_sci.c # Собирает общий автомат SCI Boot и TI Flash Kernel A для F28335 во всех портах. src/firmware_image.c src/altera_logic.c src/altera_stream.c @@ -100,6 +101,9 @@ endif() option(SETP_BUILD_TESTS "Build host tests" ON) if(SETP_BUILD_TESTS) enable_testing() + add_executable(test_c2000_sci tests/test_c2000_sci.c) # Проверяет эталонные обмены ROM/TI Kernel A и отказы до аппаратного старта. + target_link_libraries(test_c2000_sci PRIVATE setprotocol_static) # Host-проверка использует ту же реализацию, что ctypes и будущие порты. + add_test(NAME c2000_sci COMMAND test_c2000_sci) # Делает контракт штатного загрузчика F28335 частью обычного CTest. add_executable(test_deif_xdi tests/test_deif_xdi.c) # Прямые проверки C-контракта DEIF. target_link_libraries(test_deif_xdi PRIVATE setprotocol_static) # Используется единственное общее ядро. add_test(NAME deif_xdi COMMAND test_deif_xdi "${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/deif-xdi-v1.tsv") # Векторы общие с JNI и Python. diff --git a/c/set-protocol/include/c2000_sci.h b/c/set-protocol/include/c2000_sci.h new file mode 100644 index 0000000..62f92c2 --- /dev/null +++ b/c/set-protocol/include/c2000_sci.h @@ -0,0 +1,18 @@ +#ifndef SET_C2000_SCI_H /* Один публичный контракт SCI Boot обслуживает GUI и аппаратные тесты. */ +#define SET_C2000_SCI_H /* Защищает объявления от повторного включения. */ +#include "pcan_abi.h" /* Получает стабильные типы, size_t и экспорт символов общей библиотеки. */ +#ifdef __cplusplus /* C++-потребители используют тот же ABI без изменения имён функций. */ +extern "C" { /* Открывает C-связность публичных функций. */ +#endif /* Завершает условное начало C-связности. */ +PCAN_ABI_API int c2000_sci_decode(const uint8_t *source, size_t size, uint8_t *output, size_t capacity, size_t *written); /* Принимает бинарный SCI8 либо ASCII SCI8 TI со STX/ETX; не принимает Intel HEX. */ +PCAN_ABI_API int c2000_sci_validate(const uint8_t *data, size_t size, int flash, uint32_t *entry, uint16_t *sectors); /* Проверяет таблицу, адреса слов C28x, перекрытия и присутствие точки входа до передачи. */ +PCAN_ABI_API size_t c2000_sci_session_size(void); /* Возвращает размер хранилища, принадлежащего вызывающему приложению. */ +PCAN_ABI_API int c2000_sci_begin(void *storage, size_t size, const uint8_t *kernel, size_t kernel_size, const uint8_t *image, size_t image_size); /* Сохраняет указатели на неизменяемые буферы и проверяет оба образа до autobaud. */ +PCAN_ABI_API int c2000_sci_next(void *storage, uint8_t *tx, size_t capacity, size_t *tx_size, size_t *rx_size, uint32_t *delay_ms); /* Возвращает 1 для обмена, 2 для паузы, 0 после терминатора и отрицательное число при ошибке состояния. */ +PCAN_ABI_API int c2000_sci_accept(void *storage, const uint8_t *rx, size_t size); /* Сверяет эхо ROM либо checksum TI Kernel A, затем разрешает следующий шаг. */ +PCAN_ABI_API unsigned c2000_sci_progress(const void *storage); /* Возвращает завершённую долю передачи 0..100 без доступа к системным часам. */ +PCAN_ABI_API size_t c2000_sci_ulitco_enter(uint8_t *output, size_t capacity); /* Создаёт штатную команду Monitor: запись единицы в параметр FLASH_NUM=93. */ +#ifdef __cplusplus /* Закрывает C-связность только в C++-единицах трансляции. */ +} /* Завершает блок публичного C ABI. */ +#endif /* Завершает условную C++-обвязку. */ +#endif /* Завершает публичный интерфейс SCI Boot. */ diff --git a/c/set-protocol/src/c2000_sci.c b/c/set-protocol/src/c2000_sci.c new file mode 100644 index 0000000..b72e00b --- /dev/null +++ b/c/set-protocol/src/c2000_sci.c @@ -0,0 +1,106 @@ +#include "c2000_sci.h" /* Реализует независимый от ОС SCI Boot и TI Flash Kernel A. */ +#include /* Копирует уже проверенные байты без платформенных преобразований. */ +#define SCI_CHUNK_WORDS 1024U /* Буфер TI f28335_flash_kernel имеет PROG_BUFFER_LENGTH=0x400 слов. */ +typedef struct { /* Контекст не владеет образами и не выделяет динамическую память. */ + const uint8_t *kernel, *image; /* Сохраняет неизменяемые буферы на весь срок сеанса. */ + size_t kernel_size, image_size, kernel_at, image_at, pending_bytes; /* Позиции и длины измеряются физическими октетами файлов. */ + unsigned phase, wait, remaining, block_words, extra_zero; /* Отделяет autobaud, байтовое эхо ROM и порционные подтверждения Flash. */ + uint16_t expected; /* Хранит ожидаемое эхо либо сумму октетов по контракту TI Kernel A. */ +} sci_session_t; /* Размер структуры скрыт за функцией session_size для стабильного ABI. */ +static uint16_t word(const uint8_t *p) { return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); } /* SCI8 передаёт каждый 16-битный элемент младшей октетой вперёд. */ +static uint32_t address(const uint8_t *p) { return ((uint32_t)word(p) << 16) | word(p + 2); } /* Адрес состоит из старшего слова C28x, затем младшего слова. */ +static int white(uint8_t c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } /* ASCII SCI8 допускает стандартные разделители между октетами. */ +static int digit(uint8_t c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'A' && c <= 'F' ? c - 'A' + 10 : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; } /* Декодирует только шестнадцатеричные цифры TI ASCII. */ +int c2000_sci_decode(const uint8_t *source, size_t size, uint8_t *output, size_t capacity, size_t *written) { /* Приводит оба поддержанных формата к одному бинарному буферу SCI8. */ + size_t at = 0, count = 0; int high, low; /* Хранит позиции отдельно от результата до полного завершения разбора. */ + if (written) *written = 0; /* Ошибка никогда не оставляет длину частично принятого файла. */ + if (!source || !output || !written || size < 2) return -1; /* Проверяет указатели и минимальный ключ до чтения данных. */ + if (source[0] == 0xAA && source[1] == 0x08) { if (size > capacity) return -2; memmove(output, source, size); *written = size; return 0; } /* Бинарный SCI8 сохраняется побайтно. */ + while (at < size && white(source[at])) ++at; /* Пропускает пустые строки перед STX. */ + if (at == size || source[at++] != 2) return -3; /* ASCII TI начинается управляющим STX, а не Intel HEX-двоеточием. */ + for (;;) { /* Принимает только завершённый ETX поток двухзначных октетов. */ + while (at < size && white(source[at])) ++at; /* Разделители не входят в передаваемые байты. */ + if (at < size && source[at] == 3) { ++at; break; } /* ETX завершает ASCII-контейнер, не передаваясь MCU. */ + if (at + 2 > size || (high = digit(source[at])) < 0 || (low = digit(source[at + 1])) < 0) return -3; /* Усечённая либо нечисловая пара останавливает разбор. */ + if (count == capacity) return -2; /* Буфер вызывающего не может быть переполнен большим файлом. */ + output[count++] = (uint8_t)((high << 4) | low); at += 2; /* Записывает один проверенный октет. */ + if (at < size && !white(source[at]) && source[at] != 3) return -3; /* Склеенные пары и слова шире октета не допускаются. */ + } /* После ETX разрешены только пустые разделители. */ + while (at < size && white(source[at])) ++at; /* Не принимает скрытые данные после контейнера. */ + if (at != size || count < 2 || output[0] != 0xAA || output[1] != 0x08) return -3; /* Проверяет конец ASCII и ключ SCI8 до публикации длины. */ + *written = count; return 0; /* Публикует только полностью разобранный бинарный результат. */ +} /* Завершает преобразование представления SCI8. */ +int c2000_sci_validate(const uint8_t *data, size_t size, int flash, uint32_t *entry, uint16_t *sectors) { /* Проверяет адреса назначения без обращения к устройству. */ + size_t at = 22, old_at, payload; uint32_t start, stop, old_start, old_stop, target; uint16_t count, mask = 0; unsigned blocks = 0, have_entry = 0; /* SCI8 имеет 22-октетный заголовок и размеры блоков в словах. */ + if (!data || size < 24 || size % 2 || data[0] != 0xAA || data[1] != 0x08) return -4; /* Ключ 0x08AA и чётный размер обязательны для C28x SCI8. */ + target = address(data + 18); /* Поле entry находится после ключа и восьми резервных слов ROM. */ + while (at + 2 <= size) { /* Каждый блок начинается полным 16-битным размером. */ + count = word(data + at); at += 2; /* Нулевой размер обозначает конец адресной таблицы. */ + if (!count) { if (at != size || !blocks || !have_entry) return -5; if (entry) *entry = target; if (sectors) *sectors = mask; return 0; } /* Успех требует точного конца и реально загруженной точки входа. */ + payload = (size_t)count * 2; /* Один адресуемый элемент C28x занимает два октета файла. */ + if (size - at < 4 || payload > size - at - 4 || ++blocks > 256) return -5; /* Ограничивает полную секцию и число диапазонов до обращения к данным. */ + start = address(data + at); if (start > UINT32_MAX - count) return -6; stop = start + count; /* Проверяет переполнение словного диапазона. */ + if (flash) { /* Приложение SCI пишет внутреннюю Flash F28335, включая обычный аппаратный codestart. */ + if (start < 0x300000UL || stop > 0x33FFF8UL || (start < 0x33FFF6UL && stop > 0x33FF80UL)) return -6; /* Запрещает RAM, OTP, CSM reserved и пароли; разрешает только два слова codestart 33FFF6–33FFF7. */ + { uint32_t sector; for (sector = (0x33FFFFUL - (stop - 1)) / 0x8000UL; sector <= (0x33FFFFUL - start) / 0x8000UL; ++sector) mask = (uint16_t)(mask | (1U << sector)); } /* Включает каждый пересечённый сектор по 0x8000 слов; TI нумерует A младшим битом. */ + } else if (!((start < 0x800UL && stop <= 0x800UL) || (start >= 0x8000UL && stop <= 0x10000UL))) return -6; /* RAM-ядро ограничено M0/M1 и L0–L7, не затрагивая периферийные регистры. */ + old_at = 22; /* Повторно обходит предыдущие заголовки без heap и без таблицы платформенных структур. */ + while (old_at + 2 < at) { /* Все предшествующие секции уже имеют проверенные размеры. */ + uint16_t old_count = word(data + old_at); old_at += 2; /* Получает длину очередного ранее принятого блока. */ + if (old_at == at) break; old_start = address(data + old_at); old_stop = old_start + old_count; /* Текущая секция не сравнивается сама с собой. */ + if (start < old_stop && old_start < stop) return -7; old_at += 4 + (size_t)old_count * 2; /* Любое наложение запрещается независимо от совпадения байтов. */ + } /* Завершает проверку непересечения нового блока. */ + if (start <= target && target < stop) have_entry = 1; at += 4 + payload; /* Учитывает entry только внутри реально присутствующего кода. */ + } /* Отсутствующий терминатор не допускается даже при правильных секциях. */ + return -5; /* Усечённая таблица запрещена до autobaud и стирания MCU. */ +} /* Завершает проверку бинарного SCI8. */ +size_t c2000_sci_session_size(void) { return sizeof(sci_session_t); } /* Вызывающий выделяет выровненный буфер достаточного размера. */ +int c2000_sci_begin(void *storage, size_t size, const uint8_t *kernel, size_t kernel_size, const uint8_t *image, size_t image_size) { /* Начинает сеанс только после проверки обоих буферов. */ + sci_session_t *s = (sci_session_t *)storage; int status; /* Контекст размещён в памяти вызывающего порта. */ + if (!s || size < sizeof(*s)) return -1; memset(s, 0, sizeof(*s)); /* Не оставляет состояние предыдущей передачи. */ + status = c2000_sci_validate(kernel, kernel_size, 0, NULL, NULL); if (status) return status; /* В RAM можно передать только корректное ядро. */ + status = c2000_sci_validate(image, image_size, 1, NULL, NULL); if (status) return status; /* Ошибочный Flash-образ не должен даже запускать загрузку ядра. */ + s->kernel = kernel; s->kernel_size = kernel_size; s->image = image; s->image_size = image_size; return 0; /* Срок жизни буферов остаётся обязанностью порта. */ +} /* Завершает инициализацию сеанса. */ +int c2000_sci_next(void *storage, uint8_t *tx, size_t capacity, size_t *tx_size, size_t *rx_size, uint32_t *delay_ms) { /* Планирует ровно один обмен либо обязательную паузу TI. */ + sci_session_t *s = (sci_session_t *)storage; size_t count, i; /* Длины считаются октетами, кроме remaining в словах Flash. */ + if (!s || !tx || !tx_size || !rx_size || !delay_ms || s->wait || !s->kernel) return -8; *tx_size = *rx_size = 0; *delay_ms = 0; /* Запрещает новый пакет до проверки ответа на предыдущий. */ + if (s->phase == 9) return 0; /* Нулевой терминатор уже передан; аппаратная проверка чтением не заявляется. */ + if (s->phase == 2 || s->phase == 4) { ++s->phase; *delay_ms = 3000; return 2; } /* TI f05_DownloadImage выдерживает три секунды после ядра и после его autobaud. */ + if (!capacity) return -2; /* Даже одиночный символ синхронизации должен помещаться в буфер. */ + if (s->phase == 0 || s->phase == 3) { tx[0] = 'A'; *tx_size = *rx_size = 1; s->expected = 'A'; } /* ROM и Kernel A независимо определяют скорость по эху ASCII A. */ + else if (s->phase == 1) { tx[0] = s->kernel[s->kernel_at]; *tx_size = *rx_size = 1; s->expected = tx[0]; } /* ROM подтверждает каждый октет ядра своим точным эхом. */ + else if (s->phase == 7) { *rx_size = 2; s->expected = 0; } /* TI Kernel A дополнительно возвращает нулевую сумму для пустого остатка блока, кратного 1024 словам. */ + else { /* Прикладной поток подтверждается суммой порций после успешного вызова Flash API. */ + if (s->phase == 5) { count = 22; s->image_at = 0; } /* Заголовок подтверждается после стирания выбранных ядром секторов. */ + else { /* Обрабатывает полный SCI8-блок без передачи следующего до подтверждения текущего. */ + if (!s->remaining) { s->block_words = s->remaining = word(s->image + s->image_at); s->extra_zero = s->block_words > SCI_CHUNK_WORDS && s->block_words % SCI_CHUNK_WORDS == 0; } /* Учитывает точный размер порции TI и его особый пустой остаток. */ + if (!s->remaining) { if (capacity < 2) return -2; memcpy(tx, s->image + s->image_at, 2); *tx_size = 2; s->image_at += 2; s->phase = 9; return 1; } /* Терминатор завершает программирование и передаёт управление entry приложения. */ + count = (size_t)(s->remaining > SCI_CHUNK_WORDS ? SCI_CHUNK_WORDS : s->remaining) * 2; /* Полезная часть порции не превышает RAM-буфер TI. */ + if (s->remaining == s->block_words) count += 6; /* Первая порция включает размер секции и 32-битный словный адрес. */ + } /* Получена длина заголовка либо одной порции приложения. */ + if (capacity < count) return -2; memcpy(tx, s->image + s->image_at, count); *tx_size = count; *rx_size = 2; s->expected = 0; /* Копирует готовые байты без повторной сборки таблицы в порту GUI. */ + for (i = 0; i < count; ++i) s->expected = (uint16_t)(s->expected + tx[i]); s->pending_bytes = count; /* Сумма октетов по модулю 65536 совпадает с TI SendCheckSum. */ + } /* Ответ на запланированный обмен должен быть принят до следующего шага. */ + s->wait = (unsigned)*rx_size; return 1; /* Нулевой rx_size означает отправку терминатора без недостоверного ожидания ACK. */ +} /* Завершает формирование следующего действия. */ +int c2000_sci_accept(void *storage, const uint8_t *rx, size_t size) { /* Проверяет фактические ответы ROM и Flash Kernel A. */ + sci_session_t *s = (sci_session_t *)storage; unsigned words_sent; /* Состояние алгоритма меняется только после достоверного ответа. */ + if (!s || !rx || !s->wait || size != s->wait) return -8; /* Не принимает пустое, лишнее или усечённое подтверждение. */ + if ((size == 1 ? rx[0] : word(rx)) != s->expected) return -9; s->wait = 0; /* Несовпадение эха или checksum останавливает передачу. */ + if (s->phase == 0) s->phase = 1; /* После первого autobaud начинается байтовая передача RAM-ядра. */ + else if (s->phase == 1) { if (++s->kernel_at == s->kernel_size) s->phase = 2; } /* Последний подтверждённый октет ядра запускает паузу его старта. */ + else if (s->phase == 3) s->phase = 4; /* После autobaud ядра выдерживается пауза перед стиранием Flash. */ + else if (s->phase == 5) { s->image_at = 22; s->phase = 6; } /* Подтверждённый заголовок означает завершившийся вызов erase в ядре. */ + else if (s->phase == 7) s->phase = 6; /* Убирает дополнительный ACK пустого остатка до следующей секции. */ + else { /* Порция приложения подтверждена после успешного Flash_Program. */ + words_sent = (unsigned)(s->pending_bytes - (s->remaining == s->block_words ? 6 : 0)) / 2; /* Отделяет служебный заголовок секции от записанных слов. */ + s->remaining -= words_sent; s->image_at += s->pending_bytes; if (!s->remaining && s->extra_zero) s->phase = 7; /* Продвигает поток только на подтверждённое число октетов. */ + } /* Завершает успешный переход автомата. */ + return 0; /* Порт может запросить следующее действие сеанса. */ +} /* Завершает обработку подтверждения. */ +unsigned c2000_sci_progress(const void *storage) { const sci_session_t *s = (const sci_session_t *)storage; return !s || !s->image_size ? 0 : s->phase == 9 ? 100 : (unsigned)((s->kernel_at * 20) / s->kernel_size + (s->image_at * 80) / s->image_size); } /* Передача ядра занимает 20 процентов, приложение — остальные 80 процентов шкалы. */ +size_t c2000_sci_ulitco_enter(uint8_t *output, size_t capacity) { /* Использует только подтверждённую команду Monitor UlitCo из talk_with_monitor.c и param.c. */ + static const uint8_t request[10] = {247, 213, 255, 93, 0, 0, 1, 0, 0, 0}; /* Заголовок F7/D5, MONITOR_WRITE=255, FLASH_NUM=93, значение 1; текущая прошивка читает два последних октета без проверки CRC. */ + if (!output || capacity < sizeof(request)) return 0; memcpy(output, request, sizeof(request)); return sizeof(request); /* Возвращает полный штатный десятиоктетный запрос без отправки на устройство. */ +} /* Завершает общий кодек перехода UlitCo в первичный загрузчик. */ diff --git a/c/set-protocol/tests/test_c2000_sci.c b/c/set-protocol/tests/test_c2000_sci.c new file mode 100644 index 0000000..f29e901 --- /dev/null +++ b/c/set-protocol/tests/test_c2000_sci.c @@ -0,0 +1,37 @@ +#include "c2000_sci.h" /* Проверяет публичный ABI штатного SCI-загрузчика без COM и аппаратного стирания. */ +#include /* malloc предоставляет корректное выравнивание непрозрачного C-контекста. */ +#include /* Печатает место нарушенного контракта при запуске CTest. */ +#include /* Сравнивает пакеты с неизменяемыми эталонными октетами. */ +#define CHECK(condition) do { if (!(condition)) { fprintf(stderr, "SCI test line %d\n", __LINE__); exit(1); } } while (0) /* Проверки работают также в Release с NDEBUG. */ +static const uint8_t ram[] = {0xAA,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x90,2,0,0,0,0,0x90,0x12,0x34,0x56,0x78,0,0}; /* Два слова RAM L4 по адресу 0x9000 и та же точка входа. */ +static const uint8_t flash[] = {0xAA,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x32,0,0,0x80,2,0,0x32,0,0,0x80,0x12,0x34,0x56,0x78,0,0}; /* Два слова Flash сектора C по адресу 0x328000; ожидаемая маска TI равна 4. */ +static void packet(void *session, const uint8_t *expected, size_t length, size_t reply_size, const uint8_t *reply) { /* Проверяет одну эталонную передачу, не собирая пакет повторным кодеком. */ + uint8_t tx[2054]; size_t sent, received; uint32_t delay; /* Максимальная порция TI: 1024 слова и заголовок секции из шести октетов. */ + CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 1); /* Следующее действие обязано быть обменом, а не пропущенной паузой. */ + CHECK(sent == length && received == reply_size && delay == 0 && !memcmp(tx, expected, length)); /* Проверяет точное содержимое, границы и ожидаемый тип ответа. */ + if (reply_size) CHECK(c2000_sci_accept(session, reply, reply_size) == 0); /* Только достоверный ответ разрешает последующее действие. */ +} /* Завершает проверку одного шага. */ +static void pause_ti(void *session) { /* Ожидание проверяется детерминированно без системных часов и реальной трёхсекундной задержки. */ + uint8_t tx[1]; size_t sent, received; uint32_t delay; /* Пауза не требует передачи или ответа. */ + CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 2 && !sent && !received && delay == 3000); /* Длительность повторяет официальный host TI для Kernel A. */ +} /* Завершает проверку обязательной паузы. */ +int main(void) { /* Выполняет эталонный сеанс и основные ошибочные случаи на общем C-ядре. */ + void *session = malloc(c2000_sci_session_size()); uint8_t changed[sizeof(flash)], tx[2054], decoded[34]; size_t sent, received, written, i; uint32_t delay, entry; uint16_t sectors; /* Хранилище и выходные значения принадлежат host-тесту. */ + static const uint8_t sync[] = {'A'}, header_ack[] = {0x64,1}, block_ack[] = {0xC8,1}, monitor[] = {247,213,255,93,0,0,1,0,0,0}; /* Суммы эталона: заголовок 0x0164, секция 0x01C8; Monitor пишет параметр 93 значением 1. */ + static const uint8_t ascii[] = "\002 AA 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 90 02 00 00 00 00 90 12 34 56 78 00 00 \003\r\n"; /* Независимый ASCII-вектор TI содержит STX/ETX, а управляющие октеты не передаются MCU. */ + CHECK(session != NULL); CHECK(c2000_sci_validate(ram, sizeof(ram), 0, &entry, §ors) == 0 && entry == 0x9000 && sectors == 0); /* RAM-ядро должно иметь реально загруженный entry и не включать Flash-секторы. */ + CHECK(c2000_sci_validate(flash, sizeof(flash), 1, &entry, §ors) == 0 && entry == 0x328000 && sectors == 4); /* Прикладной эталон относится именно к сектору C F28335. */ + CHECK(c2000_sci_decode(ascii, sizeof(ascii) - 1, decoded, sizeof(decoded), &written) == 0 && written == sizeof(ram) && !memcmp(decoded, ram, written)); /* Проверяет точное совпадение бинарного и ASCII-представления одного образа. */ + CHECK(c2000_sci_begin(session, c2000_sci_session_size(), ram, sizeof(ram), flash, sizeof(flash)) == 0); /* Исправные буферы допускают начало autobaud. */ + packet(session, sync, 1, 1, sync); /* ROM подтверждает autobaud одним ASCII A. */ + for (i = 0; i < sizeof(ram); ++i) packet(session, ram + i, 1, 1, ram + i); /* Первичный загрузчик обязан подтвердить каждый октет RAM-ядра точным эхом. */ + pause_ti(session); packet(session, sync, 1, 1, sync); pause_ti(session); /* Ядро запускается и независимо синхронизирует UART перед erase. */ + packet(session, flash, 22, 2, header_ack); packet(session, flash + 22, 10, 2, block_ack); packet(session, flash + 32, 2, 0, NULL); /* Проверяет контрольные суммы после erase и программирования, затем терминатор без ACK. */ + CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 0 && c2000_sci_progress(session) == 100); /* Только полный подтверждённый сеанс заканчивает прогрессом 100 процентов. */ + memcpy(changed, flash, sizeof(flash)); changed[18] = changed[24] = 0x33; changed[20] = changed[26] = 0x80; changed[21] = changed[27] = 0xFF; CHECK(c2000_sci_validate(changed, sizeof(changed), 1, NULL, NULL) == -6); /* Загруженный entry не обходит запрет записи в CSM-область 0x33FF80. */ + CHECK(c2000_sci_validate(flash, sizeof(flash) - 2, 1, NULL, NULL) < 0 && c2000_sci_validate(flash, sizeof(flash), 0, NULL, NULL) < 0); /* Усечение таблицы и подмена RAM-ядра Flash-приложением отклоняются. */ + CHECK(c2000_sci_begin(session, c2000_sci_session_size(), ram, sizeof(ram), flash, sizeof(flash)) == 0); CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 1); /* Повторный независимый сеанс нужен для проверки неверного эха. */ + tx[0] = 'B'; CHECK(c2000_sci_accept(session, tx, 1) == -9 && c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == -8); /* После ошибки подтверждения автомат не должен продолжать стирание или запись. */ + CHECK(c2000_sci_ulitco_enter(tx, sizeof(tx)) == sizeof(monitor) && !memcmp(tx, monitor, sizeof(monitor))); /* Штатный переход Monitor также фиксируется эталонным wire-вектором. */ + free(session); return 0; /* Освобождает host-контекст; аппаратный интерфейс нигде не создавался. */ +} /* Завершает автономную проверку SCI Boot F28335. */ diff --git a/c/set-protocol/tools/build_host.py b/c/set-protocol/tools/build_host.py index 2362ff5..64ea8ff 100644 --- a/c/set-protocol/tools/build_host.py +++ b/c/set-protocol/tools/build_host.py @@ -24,6 +24,7 @@ INCLUDE = ROOT / "include" JNI_INCLUDES: list[Path] = [] SOURCES = [ ROOT / "src" / name for name in ( + "c2000_sci.c", # Прямые MSVC/CC-сборки экспортируют общий автомат штатного SCI-загрузчика F28335. "deif_xdi.c", # Общий DEIF входит в host-библиотеку. "set_invertor_gas.c", # Собирает общий адаптер двух GAS для STM Invertor при прямом запуске host-builder. "set_deif_gauge.c", "set_igbt_realtime.c", diff --git a/python/protocan/c2000_sci.py b/python/protocan/c2000_sci.py new file mode 100644 index 0000000..18e6db4 --- /dev/null +++ b/python/protocan/c2000_sci.py @@ -0,0 +1,63 @@ +"""Тонкий ctypes-порт общего автомата SCI Boot F28335 и TI Flash Kernel A.""" # Формат таблицы, checksum и порядок обменов реализованы только в C99 templates. +import ctypes # Передаёт байтовые буферы и состояние через стабильный C ABI. +from .native import get_native_protocol # Использует ту же общую библиотеку, что остальные протоколы SETGUI. + + +def _api(): # Настраивает только сигнатуры новых функций без изменения существующего ABI. + lib = get_native_protocol().lib # Отсутствующее C-ядро остаётся явной ошибкой сборки приложения. + pointer, size, uint = ctypes.c_void_p, ctypes.c_size_t, ctypes.c_uint32 # Фиксированные типы отделяют порт от внутренней структуры C. + try: # Старый runtime должен сообщить о необходимости обновления вместо Python-fallback протокола. + signatures = {"c2000_sci_decode": ([pointer, size, pointer, size, ctypes.POINTER(size)], ctypes.c_int), "c2000_sci_validate": ([pointer, size, ctypes.c_int, ctypes.POINTER(uint), ctypes.POINTER(ctypes.c_uint16)], ctypes.c_int), "c2000_sci_session_size": ([], size), "c2000_sci_begin": ([pointer, size, pointer, size, pointer, size], ctypes.c_int), "c2000_sci_next": ([pointer, pointer, size, ctypes.POINTER(size), ctypes.POINTER(size), ctypes.POINTER(uint)], ctypes.c_int), "c2000_sci_accept": ([pointer, pointer, size], ctypes.c_int), "c2000_sci_progress": ([pointer], ctypes.c_uint), "c2000_sci_ulitco_enter": ([pointer, size], size)} # Сигнатуры соответствуют публичному c2000_sci.h. + for name, (arguments, result) in signatures.items(): # Настройка повторно безопасна для нескольких форм загрузчика. + function = getattr(lib, name) # Все символы обязаны присутствовать в собранной общей библиотеке. + function.argtypes, function.restype = arguments, result # ctypes проверяет типы и не усечёт указатели на x64. + except AttributeError as error: raise RuntimeError("Обновите библиотеку setprotocol: поддержка C2000 SCI отсутствует") from error # Запрещает незаметную подмену общего алгоритма кодом порта. + return lib # Возвращает библиотеку с проверенными экспортами нового протокола. + + +def _check(status): # Переводит детерминированные ошибки C в локализованное сообщение интерфейса. + messages = {-1: "Неверные параметры SCI", -2: "Недостаточный буфер SCI", -3: "Неверный ASCII SCI8 TI", -4: "Неверный заголовок SCI8", -5: "Обрезанная таблица SCI8 или отсутствующая точка входа", -6: "SCI8 содержит недопустимые адреса RAM/Flash или область CSM", -7: "SCI8 содержит пересекающиеся секции", -8: "Нарушена последовательность SCI Boot", -9: "Эхо или контрольная сумма загрузчика не совпали"} # Сообщения не повторяют проверку пакетов или вычисление checksum. + if status < 0: raise ValueError(messages.get(status, "Ошибка C2000 SCI: " + str(status))) # Любой отказ C останавливает передачу в аппаратном порту. + + +def decode(data): # Преобразует представление SCI8 общим C-декодером до работы с COM. + lib, raw = _api(), ctypes.create_string_buffer(data) # Сохраняет исходные байты на время вызова C. + output, written = ctypes.create_string_buffer(max(1, len(data))), ctypes.c_size_t() # Декодированный поток никогда не длиннее ASCII-источника. + _check(lib.c2000_sci_decode(raw, len(data), output, len(output), ctypes.byref(written))) # Проверку STX/ETX и шестнадцатеричных октетов выполняет общий модуль. + return output.raw[:written.value] # Возвращает только опубликованную C длину полного результата. + + +def validate(data, flash=True): # Получает сведения об образе без повторного разбора его полей в Python. + raw, entry, sectors = ctypes.create_string_buffer(data), ctypes.c_uint32(), ctypes.c_uint16() # Указатели результата имеют фиксированную разрядность ABI. + _check(_api().c2000_sci_validate(raw, len(data), int(flash), ctypes.byref(entry), ctypes.byref(sectors))) # Адреса, перекрытия и entry проверяются до загрузки ядра. + return entry.value, sectors.value # Адрес выражен словами C28x, sectors — маской TI Flash API. + + +def ulitco_enter(): # Возвращает штатный пакет перехода приложения UlitCo в загрузчик. + output = ctypes.create_string_buffer(10) # Формат текущего Monitor содержит ровно десять октетов. + size = _api().c2000_sci_ulitco_enter(output, len(output)) # Адрес параметра и формат команды остаются в общем C-ядре. + if size != 10: raise ValueError("Не удалось подготовить команду Monitor UlitCo") # Не передаёт устройству частичный управляющий запрос. + return output.raw[:size] # Порт отправляет байты без повторной сборки кадра. + + +class Session: # Владеет C-контекстом и буферами двух образов на всё время прошивки. + def __init__(self, kernel, image): # Проверяет оба образа до первого аппаратного действия. + self.lib = _api() # Одна библиотека обслуживает все шаги данного сеанса. + self.kernel, self.image = ctypes.create_string_buffer(kernel), ctypes.create_string_buffer(image) # Буферы не перемещаются и не освобождаются до завершения сеанса. + size = self.lib.c2000_sci_session_size() # Размер внутренней C-структуры не дублируется в ctypes. + self.storage = (ctypes.c_uint64 * ((size + 7) // 8))() # Выравнивает непрозрачный контекст минимум на восемь октетов для указателей x64. + _check(self.lib.c2000_sci_begin(self.storage, ctypes.sizeof(self.storage), self.kernel, len(kernel), self.image, len(image))) # Общая инициализация запрещает ошибочные таблицы до autobaud. + + def next(self): # Получает готовое действие от детерминированного C-автомата. + tx, tx_size, rx_size, delay = ctypes.create_string_buffer(2054), ctypes.c_size_t(), ctypes.c_size_t(), ctypes.c_uint32() # 1024 слова и шесть октетов заголовка секции покрывают максимальную порцию TI. + state = self.lib.c2000_sci_next(self.storage, tx, len(tx), ctypes.byref(tx_size), ctypes.byref(rx_size), ctypes.byref(delay)) # Порт не вычисляет длину wire-пакета или checksum. + _check(state) # Ошибка последовательности запрещает дальнейшую передачу. + return state, tx.raw[:tx_size.value], rx_size.value, delay.value # Возвращает только операционные параметры ввода-вывода. + + def accept(self, reply): # Передаёт полный ответ для проверки общему C-ядру. + raw = ctypes.create_string_buffer(reply) # Сохраняет исходные октеты без преобразования в Python-число checksum. + _check(self.lib.c2000_sci_accept(self.storage, raw, len(reply))) # Успешная проверка разрешает следующий шаг автомата. + + @property # Прогресс отражает только подтверждённые этапы передачи. + def progress(self): # Представление не повторяет состояния алгоритма в Python. + return int(self.lib.c2000_sci_progress(self.storage)) # Значение 0..100 пригодно для штатной полосы GUI. diff --git a/python/set_devices/qt_ports/c2000_sci_panel.py b/python/set_devices/qt_ports/c2000_sci_panel.py new file mode 100644 index 0000000..239ec24 --- /dev/null +++ b/python/set_devices/qt_ports/c2000_sci_panel.py @@ -0,0 +1,133 @@ +"""Встроенная прошивка F28335 через штатный SCI Boot без внешнего C2Prog.""" # Qt отвечает за COM и потоки; протокол реализован общим C99-ядром templates. +import hashlib # Проверяет точные байты RAM-ядра, сохранённого в комплекте postbuild. +from pathlib import Path # Разрешает имя RAM-ядра относительно выбранного BIN. +import threading # Передаёт отмену из GUI в рабочий поток без принудительного завершения потока. +import time # Системные часы используются только транспортом для конечных тайм-аутов ожидания. +try: # Поддерживает основные сборки Qt 6 и Windows 7/Qt 5. + from PySide6.QtCore import QThread, Signal, QIODevice # Рабочий поток не блокирует GUI ожиданием UART или Flash API. + from PySide6.QtWidgets import QWidget, QFormLayout, QLineEdit, QPushButton, QCheckBox, QSpinBox, QFileDialog, QLabel # Панель использует обычные контролы SETGUI. + from PySide6.QtSerialPort import QSerialPort # Транспорт создаёт отдельный системный COM-сеанс на время программирования. +except ImportError: # Для Python 3.8/Windows 7 использует те же операции Qt 5. + from PySide2.QtCore import QThread, Signal, QIODevice # Сигналы безопасно возвращают результаты в главный поток. + from PySide2.QtWidgets import QWidget, QFormLayout, QLineEdit, QPushButton, QCheckBox, QSpinBox, QFileDialog, QLabel # Интерфейс не требует компонентов, доступных только Qt 6. + from PySide2.QtSerialPort import QSerialPort # COM-порт остаётся локальной зависимостью адаптера. +from protocan.c2000_sci import Session, decode, validate, ulitco_enter # Сборка, разбор, checksum и автомат обмена не повторяются в порту. + + +class SciWorker(QThread): # Один сеанс владеет COM-портом и неизменяемыми буферами до завершения. + progress = Signal(int, str) # Штатная полоса GUI получает только подтверждённый прогресс C-автомата. + result = Signal(bool, str) # Ошибка транспорта или checksum не выдаётся за успешную запись. + + def __init__(self, kernel, image, port, baud, enter=False, timeout_ms=3000, parent=None): # Все параметры снимаются с GUI до запуска потока. + super().__init__(parent) # Время жизни потока связано с существующей панелью прошивки. + self.kernel, self.image, self.port, self.baud = kernel, image, port, baud # Буферы не перечитываются с диска после аппаратного старта. + self.enter, self.timeout_ms, self.stop = enter, timeout_ms, threading.Event() # Отмена проверяется во всех конечных ожиданиях. + + def cancel(self): # GUI не вызывает методы QSerialPort чужого потока. + self.stop.set() # Рабочий поток сам завершит чтение и закроет COM-порт. + + def _cancelled(self): # Единая проверка отмены используется в задержках, чтении и записи. + if self.stop.is_set(): raise ValueError("Прошивка остановлена оператором") # Отмена не запускает повторную передачу или попытку erase. + + def _pause(self, milliseconds): # Обязательные паузы TI остаются прерываемыми оператором. + end = time.monotonic() + milliseconds / 1000 # Миллисекунды C-автомата преобразуются только в системное ожидание порта. + while time.monotonic() < end: self._cancelled(); self.msleep(min(50, max(1, int((end - time.monotonic()) * 1000)))) # Проверяет отмену не реже одного раза за 50 мс. + + def _write(self, serial, data): # Передаёт готовые байты и дожидается фактической отправки UART. + self._cancelled() # Не добавляет новые данные после запроса остановки. + if not data: return # Отдельное подтверждение пустого остатка TI не требует передачи. + if serial.write(data) != len(data): raise OSError("Не удалось поставить полный пакет SCI в очередь COM") # Частичная запись не должна сдвинуть автомат протокола. + end = time.monotonic() + max(5, len(data) * 12 / self.baud + 2) # Бюджет включает время передачи октетов UART и запас драйвера. + while serial.bytesToWrite(): # Очередь обязана опустеть до начала ожидания ответа. + self._cancelled() # Отмена сохраняет конечность длительной отправки на низкой скорости. + serial.waitForBytesWritten(100) # Короткое ожидание позволяет проверять отмену и ошибку драйвера. + if time.monotonic() > end: raise TimeoutError("Истёк тайм-аут передачи SCI") # Не ожидает бесконечно отключённый преобразователь. + + def _read(self, serial, count, timeout): # Собирает ровно требуемое число октетов, не разбирая их поля. + result, end = bytearray(), time.monotonic() + timeout / 1000 # Единственный дедлайн сохраняется при частичных UART-порциях. + while len(result) < count: # Фрагментация USB-UART не меняет смысл ответа C-автомата. + self._cancelled() # Оператор может прекратить ожидание недоступного MCU. + if serial.bytesAvailable(): result.extend(bytes(serial.read(count - len(result)))) # Следующий ответ не поглощается текущим чтением. + else: serial.waitForReadyRead(100) # Ожидание остаётся внутри рабочего потока, без зависания GUI. + if time.monotonic() > end: raise TimeoutError("Загрузчик F28335 не подтвердил обмен SCI") # Отсутствие эха или успешного Flash API не трактуется как продолжение. + return bytes(result) # Проверку эха и checksum выполнит C-ядро. + + def run(self): # Qt вызывает метод в отдельном потоке, где создаётся и закрывается COM-порт. + serial = None # Ошибка проверки образа не должна даже открывать аппаратный интерфейс. + try: # Все исключения превращаются в конечный результат GUI. + session = Session(self.kernel, self.image) # Проверяет RAM-ядро и Flash-таблицу до любой управляющей команды. + serial = QSerialPort(); serial.setPortName(self.port); serial.setBaudRate(self.baud) # Использует точный COM-порт и скорость из формы/manifest. + serial.setDataBits(QSerialPort.DataBits.Data8); serial.setParity(QSerialPort.Parity.NoParity); serial.setStopBits(QSerialPort.StopBits.OneStop); serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) # TI SCI Boot и первичный загрузчик UlitCo используют 8N1 без аппаратного управления потоком. + if not serial.open(QIODevice.OpenModeFlag.ReadWrite): raise OSError(serial.errorString()) # Занятый терминалом порт не открывается вторым владельцем. + serial.clear() # Старые данные терминала не должны стать ложным эхо autobaud. + if self.enter: # Приложение UlitCo принимает переход в загрузчик лишь в разрешённом остановленном состоянии. + self.progress.emit(0, "Переход UlitCo в первичный загрузчик…") # Показывает отдельный этап до передачи RAM-ядра. + self._write(serial, ulitco_enter()); self._pause(500); serial.clear() # Штатная команда Monitor №93 не подменяется BIOS ПМ35. + while True: # Порядок обменов полностью определяется общим C-автоматом. + self._cancelled(); state, tx, expected, delay = session.next() # Порт не вычисляет пакетные поля или checksum. + if state == 0: break # Подтверждённые порции и отправленный терминатор завершили поток. + if state == 2: self._pause(delay); serial.clear(); continue # TI требует паузу после RAM-ядра и его autobaud. + self._write(serial, tx) # Отправляет готовый октет ROM либо порцию Flash Kernel A. + if expected: session.accept(self._read(serial, expected, 60000 if expected == 2 else self.timeout_ms)) # Erase/program получают минуту; эхо ROM использует точный тайм-аут из профиля JSON в миллисекундах. + self.progress.emit(session.progress, "Ядро TI / запись Flash…") # Продвижение следует только успешно проверенным ответам. + self.result.emit(True, "Передача завершена; ядро TI подтвердило порции Flash. Проверка чтением памяти отдельно не выполнялась.") # Не заявляет недоступную в Kernel A полную readback-проверку. + except (OSError, ValueError, RuntimeError) as error: self.result.emit(False, str(error)) # Ошибка остановит GUI и сохранится в журнале текущей прошивки. + finally: # Владение портом освобождается также после отмены или тайм-аута. + if serial is not None: serial.close() # Закрывает только созданный этим потоком COM-сеанс. + + +class C2000SciPanel(QWidget): # Общая панель используется SETGUI без зависимости от отдельного приложения C2Prog. + progress = Signal(int, str) # Согласует рабочий поток со штатной полосой прошивки. + finished = Signal(bool, str) # Завершение освобождает форму одинаково после успеха, отказа или отмены. + + def __init__(self, parent=None): # Создаёт только параметры маршрута, оставляя COM и скорость основной форме. + super().__init__(parent); self.worker = None; self.metadata = {}; self.path = None # Состояние соответствует одному выбранному Flash BIN. + form = QFormLayout(self) # Контролы не требуют дополнительного окна приложения. + self.kernel = QLineEdit(); form.addRow("RAM-ядро TI", self.kernel) # Готовый комплект заполняет путь автоматически. + button = QPushButton("Выбрать ядро…"); button.clicked.connect(self._choose); form.addRow(button) # Старые образы допускают явный выбор соответствующего ядра. + self.enter_monitor = QCheckBox("Перевести остановленный UlitCo в загрузчик через Monitor №93"); form.addRow(self.enter_monitor) # Управляющая команда отправляется только при явном выбранном режиме. + self.oscillator_hz = QSpinBox(); self.oscillator_hz.setRange(20000000, 30000000); self.oscillator_hz.setSingleStep(5000000); self.oscillator_hz.setValue(30000000); self.oscillator_hz.setReadOnly(True); form.addRow("Кварц ядра, Гц", self.oscillator_hz) # Частота зафиксирована при сборке ядра и не меняется настройкой COM. + self.timeout_ms = QSpinBox(); self.timeout_ms.setRange(50, 10000); self.timeout_ms.setValue(3000); form.addRow("Ожидание связи, мс", self.timeout_ms) # Отдельный erase/program получает обязательный бюджет до 60 секунд. + label = QLabel("Штатный SCI Boot F28335: приложение UlitCo → RAM-ядро TI → Flash. Выберите порт SCI-A; при ручном входе предварительно включите загрузчик."); label.setWordWrap(True); form.addRow(label) # Оператор видит физический интерфейс и последовательность до начала записи. + + def _choose(self): # Выбор RAM-ядра сам по себе ничего не отправляет в устройство. + path, _ = QFileDialog.getOpenFileName(self, "RAM-ядро F28335 SCI", self.kernel.text(), "SCI8 (*.bin *.txt)") # Принимает штатные бинарное и ASCII-представления TI. + if path: self.kernel.setText(path) # Отмена сохраняет текущий комплект. + + def set_manifest(self, image, metadata): # Подтягивает ядро и параметры из выбранного постсборочного комплекта. + self.path, self.metadata = Path(image), metadata # Сохраняет принадлежность ядра конкретному Flash BIN. + info = metadata.get("flash_kernel", {}); info = info if isinstance(info, dict) else {} # Не относящийся к SCI manifest не должен нарушать визуальную загрузку других режимов. + name = info.get("file", "") # Manifest хранит только исходное имя соседнего RAM-бинарника. + if isinstance(name, str) and name and Path(name).name == name: self.kernel.setText(str(self.path.parent / name)) # Не разрешает metadata-путь за пределы папки образа. + else: self.kernel.clear() # Новый образ не должен наследовать RAM-ядро предыдущей прошивки. + frequency = info.get("oscillator_hz", 30000000); self.oscillator_hz.setValue(frequency if type(frequency) is int and frequency in (20000000, 25000000, 30000000) else 30000000) # Показывает допустимую частоту без исключения setter на чужих metadata. + + def prepare(self, image): # Фиксирует и проверяет все байты до подтверждения аппаратной операции. + with Path(image).open("rb") as stream: data = stream.read(1024 * 1024 + 1) # Ограничивает чтение Flash BIN до одного МиБ и одного контрольного октета. + with Path(self.kernel.text()).open("rb") as stream: kernel_raw = stream.read(1024 * 1024 + 1) # Worker получает ограниченный неизменяемый снимок RAM-ядра. + if len(data) > 1024 * 1024 or len(kernel_raw) > 1024 * 1024: raise ValueError("Слишком большой файл SCI8") # Ограничивает отдельные Flash и RAM-образы одним МиБ. + if self.metadata and (len(data) != self.metadata.get("size_bytes") or hashlib.sha256(data).hexdigest() != self.metadata.get("sha256")): raise ValueError("Flash BIN изменён после выбора или не соответствует manifest") # Новый корректный SCI8 не должен незаметно заменить ранее подтверждённый образ. + info = self.metadata.get("flash_kernel", {}) # При наличии паспорта зависимость должна совпадать побайтно. + if not isinstance(info, dict): raise ValueError("В manifest требуется объект flash_kernel") # Неверный тип метаданных возвращается обычной ошибкой комплекта до доступа к COM. + if info and (len(kernel_raw) != info.get("size_bytes") or hashlib.sha256(kernel_raw).hexdigest() != info.get("sha256")): raise ValueError("RAM-ядро TI не соответствует manifest Flash BIN") # Чужая частота или маска erase не должны незаметно попасть в сеанс. + kernel, flash = decode(kernel_raw), decode(data) # Оба представления разбираются только общим C-декодером. + validate(kernel, False); _, sectors = validate(flash, True) # Не открывает COM для повреждённых таблиц или адресов CSM; маску вычисляет единственный C-декодер. + if info and (info.get("sector_mask") != sectors or info.get("oscillator_hz") != self.oscillator_hz.value()): raise ValueError("RAM-ядро собрано для другой частоты или набора секторов Flash") # Комплект обязан соответствовать фактическим адресам приложения и выбранному при сборке кварцу. + return kernel, flash # Возвращает готовые байты для неизменяемой аппаратной операции. + + def start(self, job, port, baud): # Основная форма вызывает метод после штатного подтверждения пользователя. + if self.worker is not None: raise ValueError("Прошивка SCI уже выполняется") # Один виджет не может владеть двумя одновременными COM-сеансами. + self.worker = SciWorker(*job, port, baud, self.enter_monitor.isChecked(), self.timeout_ms.value(), self) # Worker получает только снимок параметров главного потока. + self._result = None; self.worker.progress.connect(self.progress.emit); self.worker.result.connect(self._capture_result); self.worker.finished.connect(self._released) # Разблокирует форму только после finally рабочего потока и фактического закрытия COM. + self.worker.start() # До этого места никакие байты MCU не передавались. + + def _capture_result(self, success, text): # Промежуточный сигнал не означает, что QThread уже закончил закрытие порта. + self._result = (success, text) # Конечное уведомление будет отправлено после штатного QThread.finished. + + def _released(self): # Ссылка на поток сохраняется до завершения его finally и закрытия COM. + worker, self.worker = self.worker, None # После окончания разрешает следующую явную прошивку. + if worker is not None: worker.deleteLater() # Освобождает Qt-объект в главном потоке без уничтожения работающего QThread. + self.finished.emit(*(self._result or (False, "Поток SCI завершился без результата"))) # Форма снова доступна, когда собственный COM-сеанс гарантированно закрыт. + + def cancel(self): # Отмена не вызывает terminate и не убивает чужие процессы. + if self.worker is not None: self.worker.cancel() # Рабочий поток завершит своё ближайшее конечное ожидание. diff --git a/python/tests/test_c2000_sci.py b/python/tests/test_c2000_sci.py new file mode 100644 index 0000000..855da8e --- /dev/null +++ b/python/tests/test_c2000_sci.py @@ -0,0 +1,51 @@ +"""Эталонные обмены SCI ROM и TI Flash Kernel A без настоящей платы.""" # Проверки не открывают COM и не вызывают внешнюю программу C2Prog. +import struct # Формирует независимые тестовые SCI8-векторы с явно заданными словами и адресами. +import pytest # Проверяет отказ при повреждениях и граничные размеры порций ядра TI. +from protocan.c2000_sci import Session, decode, validate, ulitco_enter # Испытывает настоящий ctypes-порт общего C99-ядра. + + +def sci8(address, words): # Готовит минимальную адресную таблицу для эталонных данных теста. + pointer = struct.pack("> 16, address & 65535) # SCI8 хранит старшее слово адреса перед младшим. + return b"\xaa\x08" + bytes(16) + pointer + struct.pack(" 1024 and words % 1024 == 0) # Обрабатывает особенность исходного TI Kernel A на кратной порции. + + +def test_corrupt_echo_and_checksum_stop_transfer(): # Повреждённое подтверждение не должно разрешать следующий пакет. + kernel, image = sci8(0x9000, [1]), sci8(0x328000, [2]) # Минимальные векторы отделяют проверку ответа от больших файлов. + session = Session(kernel, image); session.next() # Первый пакет требует эхо ASCII A. + with pytest.raises(ValueError, match="не совпали"): session.accept(b"B") # Чужой байт autobaud не принимается как связь с ROM. + session = Session(kernel, image) # Независимый сеанс проверяет повреждение двухоктетной суммы Flash. + while True: # Доходит до первой операции erase/header без использования внутренних фаз автомата. + state, tx, expected, _ = session.next() # Паузы не требуют аппаратного ответа. + if state == 2: continue # Номер стадии остаётся внутри C-ядра. + if expected == 1: session.accept(tx) # Корректный ROM позволяет загрузить RAM-ядро. + else: break # Первый двухоктетный ответ должен подтвердить заголовок приложения. + with pytest.raises(ValueError, match="не совпали"): session.accept(b"\xff\xff") # Неверная сумма не разрешает программирование следующей секции. + + +def test_ascii_decode_memory_ranges_and_monitor_command(): # Проверяет переносимость форматов и аппаратные границы до открытия порта. + image = sci8(0x328000, [0x1234]) # Обычный Flash-вектор не пересекает CSM. + ascii_image = b"\x02\n" + " ".join(f"{value:02X}" for value in image).encode("ascii") + b"\n\x03\n" # Формат соответствует выводу TI hex2000 -boot -sci8 -a. + assert decode(ascii_image) == image and validate(image) == (0x328000, 4) # Сектор C имеет маску 0x0004 в TI Flash API. + with pytest.raises(ValueError): decode(ascii_image[:-2]) # Усечённый ETX не должен оставить частично декодированное ядро. + with pytest.raises(ValueError): validate(sci8(0x33FFF8, [0x1234])) # Область CSM-паролей запрещена независимо от имени файла. + with pytest.raises(ValueError): validate(sci8(0x8000, [0x1234])) # RAM-образ не может стать Flash-приложением из-за выбранного loader. + assert ulitco_enter() == bytes([247, 213, 255, 93, 0, 0, 1, 0, 0, 0]) # Эталон совпадает с десятиоктетным Monitor-запросом FLASH_NUM=93 текущей прошивки UlitCo. diff --git a/tools/firmware-build/README.md b/tools/firmware-build/README.md index e5531db..66aa8ba 100644 --- a/tools/firmware-build/README.md +++ b/tools/firmware-build/README.md @@ -1,5 +1,131 @@ # Подключение precompile и postcompile из templates +## Параметры прошивки CAN и RS из JSON + +Настройка CCS/Keil в SETGUI теперь проверяет Python до изменения проекта. +Общие BAT вызывают `python-runtime.ps1`: он находит Python 3.8+ в установленном +окружении либо готовит проверенный переносимый runtime в +`%LOCALAPPDATA%/SET/FirmwareBuild`. Ручная настройка PATH не требуется. +Для Windows 10/11 x64 используется закреплённый Python 3.13.12, для Windows 7 +и x86 — Python 3.8.10. ZIP загружается с python.org с проверкой SHA-256; +повторные сборки используют готовую локальную копию. При отсутствии доступа +к сети можно заранее установить Python либо задать полный путь `FIRMWARE_PYTHON`. + +Для **штатного SCI-загрузчика UlitCo / C2Prog** выберите в настройке проекта +маршрут `f28335-sci`. Он автоматически подключает общую реализацию из +`c/firmware-info/tools/c2000_sci_build.py`; отдельный проектный `build_flash.py` +не требуется. Пример блока: + +```json +"flashing": { + "default_loader": "f28335-sci", + "profiles": { + "f28335-sci": { + "baudrate": 115200, + "oscillator_hz": 30000000, + "enter_monitor": false, + "timeout_ms": 3000 + } + } +} +``` + +Укажите фактическую частоту кварца: поддержаны 20, 25 и 30 МГц. Она определяет +времена Flash API и сохраняется вместе с RAM-ядром. Установленный C2000Ware с +`device_support/f2833x/examples/f28335_flash_kernel` определяется автоматически +в `C:/ti` либо по `C2000WARE_ROOT`. Эта зависимость проверяется при применении +настройки проекта. TI compiler берётся из `${CG_TOOL_ROOT}` текущей CCS. + +Postbuild проверяет Flash LOAD-секции исходного OUT и выпускает +`*_flash_rs.bin`, `*_flash.out`, `f28335_sci_kernel.bin`, manifest, SHA-256 +и уведомление о лицензии TI. Ядро собирается из штатных исходников SDK под +частоту платы и маску секторов именно текущего приложения. Основной manifest +в `latest_build.json` указывает сразу на Flash SCI BIN; папка `eeprom` для +этого маршрута не создаётся. + +В SETGUI маршрут работает внутри общей вкладки прошивки: первичный загрузчик +принимает RAM-ядро TI с побайтовым эхом, затем Kernel A стирает выбранные сектора +и принимает порции Flash с подтверждением checksum после Flash API. Внешний +C2Prog не запускается. Тайм-аут, несовпадение эха/суммы и отмена завершают +операцию; readback-проверка памяти этим протоколом Kernel A не заявляется. +Ядро автоматически выбирается из manifest и проверяется по SHA-256. + +`enter_monitor=true` включает штатную команду UlitCo: запись `1` в параметр +Monitor №93 перед autobaud. Действующая прошивка разрешает такой переход только +в остановленном состоянии. При `false` предварительно переведите плату в +первичный SCI-загрузчик обычным способом. Это отдельный маршрут относительно +`f28335-flash-rs`, который использует BIOS ПМ35 и защищённое приложение FAB1. + +В проектном `firmware_identity.json` можно хранить блок `flashing` вместе с +версией. Keil и CCS проверяют его до увеличения номера сборки и переносят в +`generated/firmware_identity.json`, каждый `*.manifest.json` и архив выпуска. +Параметры описывают **загрузку образа**; скорость работающей программы на MCU +по-прежнему задаётся её исходниками и настройками аппаратуры. + +```json +"flashing": { + "default_loader": "set-can", + "profiles": { + "set-can": {"bitrate": 250000, "node_id": 13}, + "set-rs485": {"baudrate": 115200} + } +} +``` + +`default_loader` — конкретный загрузчик, выбираемый при импорте сборки или +локального образа; его ключ должен присутствовать в `profiles`. Каждая запись +`profiles` задаёт независимые параметры соответствующего загрузчика. Значения +примеров нужно заменить параметрами своей платы. JSON не допускает комментарии; +назначение полей приведено ниже. + +| Загрузчик | Поддерживаемые параметры | +|---|---| +| `set-can` | `bitrate`, `node_id`, `product_type`, `hardware_min`, `hardware_max`, `base_address` | +| `protocan-can` | Поля `set-can`, кроме `base_address`; дополнительно `device_type`, `session_id`, `memory` | +| `set-rs485`, `stm32-uart` | `baudrate`, `base_address` | +| `f28335-eeprom`, `f28335-flash-rs` | `baudrate`, `controller`, `timeout_ms` | +| `stm32-usb` | `base_address` | +| `manual`, `f28335-jtag` | Пустой объект `{}` | + +| Поле | Единицы и ограничения | +|---|---| +| `bitrate` | Целое число бит/с, `1..1000000`; верхняя граница Classic CAN. Например, `250000` — 250 кбит/с. Адаптер также должен поддерживать выбранную скорость. | +| `baudrate` | Целое число бод, `1..2147483647`; фактическую поддержку проверяет COM-драйвер. Например, `115200`. | +| `node_id` | Адрес SETP `0..255`; для ProtoCAN — поле Device `0..15`. | +| `device_type`, `session_id` | Поля существующего ProtoCAN-загрузчика: Type `0..7`, Session ID `1..255`. | +| `product_type` | Код совместимости `0..65535`; например, `358` соответствует `0x0166`, а `10291` — `0x2833`. | +| `hardware_min`, `hardware_max` | Включительный диапазон аппаратных ревизий `0..255`; min не больше max. | +| `base_address` | Целочисленный адрес приложения в **байтах**, `0..4294967295`. Например, `134230016` соответствует `0x08003000`; это адрес примера, а не общий адрес всех STM32. Для HEX должен совпадать с адресом самого образа. | +| `controller` | Независимый адрес контроллера RS-протокола ПМ35, `0..255`. | +| `timeout_ms` | Дополнительный тайм-аут RS-панели ПМ35 в миллисекундах, `50..10000`. | +| `memory` | Выбор назначения ProtoCAN: `eeprom`, `flash` или `stm32`. | + +Формат UART определяется протоколом: SET RS использует 8N1, STM32 ROM Boot — +8E1. COM-порт, экземпляр CAN-адаптера и его соединение выбираются на компьютере +оператора, поскольку они не являются свойствами образа прошивки. + +Обновлённый SETGUI выбирает загрузчик и заполняет форму из manifest при импорте +postbuild и выборе опубликованного комплекта. Общая вкладка прошивки также +подхватывает настройки при обычном выборе локального BIN/HEX, если рядом лежит +соответствующий `имя.bin.manifest.json` или `имя.hex.manifest.json`; перед +применением проверяются имя, размер и SHA-256 файла. Маршрут выбранной записи +каталога использует свой профиль, даже если `default_loader` задаёт другой. +При переключении общей формы между CAN и RS применяется профиль нового режима. +Локальный выбор BIN в панели F28335 подхватывает профиль выбранного RS-загрузчика +после штатной проверки образа; JTAG сохраняет отдельное подключение. +Скорость уже открытого канала применяется после его отключения. Запись платы +запускается отдельной штатной кнопкой. + +Для отдельных CAN BIN и Flash RS BIN C2000 назначение из паспорта артефакта +имеет приоритет над `default_loader` основного EEPROM-образа. Профили при этом +остаются общими для комплекта. Старые конфигурации без `flashing` сохраняют +прежнее поведение; существующие проектные JSON автоматически не заменяются. + +Полные примеры: [Keil](keil/firmware_identity.example.json) и +[CCS](ccs/firmware_identity.example.json). Поддержка чтения профилей должна быть +включена в сборку SETGUI вместе с обновлённым модулем +`set_devices.firmware_flashing` из templates. + Для проектов ПЛИС используйте [шаблон Xilinx ISE, Vivado и Altera/Intel Quartus](fpga/README.md): общий сохраняемый итератор, паспорт на Verilog/VHDL, полная сборка `.bit`/`.sof`, manifest, SHA-256 и архив выпуска. diff --git a/tools/firmware-build/ccs/firmware_identity.example.json b/tools/firmware-build/ccs/firmware_identity.example.json index 1faef6a..e249739 100644 --- a/tools/firmware-build/ccs/firmware_identity.example.json +++ b/tools/firmware-build/ccs/firmware_identity.example.json @@ -2,5 +2,36 @@ "product": "MY_CONTROLLER", "target": "TMS320F28335", "version": [0, 1, 0], - "increment_version": true + "increment_version": true, + "flashing": { + "default_loader": "f28335-eeprom", + "profiles": { + "f28335-eeprom": { + "baudrate": 115200, + "controller": 16, + "timeout_ms": 500 + }, + "f28335-flash-rs": { + "baudrate": 115200, + "controller": 16, + "timeout_ms": 500 + }, + "f28335-sci": { + "baudrate": 115200, + "oscillator_hz": 30000000, + "enter_monitor": false, + "timeout_ms": 3000 + }, + "protocan-can": { + "bitrate": 250000, + "device_type": 1, + "node_id": 8, + "session_id": 1, + "product_type": 10291, + "hardware_min": 1, + "hardware_max": 1, + "memory": "eeprom" + } + } + } } diff --git a/tools/firmware-build/ccs/postcompile.bat b/tools/firmware-build/ccs/postcompile.bat index 14c730c..bcbcf16 100644 --- a/tools/firmware-build/ccs/postcompile.bat +++ b/tools/firmware-build/ccs/postcompile.bat @@ -19,11 +19,13 @@ if "%~6"=="" goto usage if not "%~8"=="" goto usage if "%~7"=="" goto identity_local rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI. -python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" --export "%~f7" +rem Тот же общий runtime обслуживает postbuild и экспорт текущего проверенного выпуска. +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" --export "%~f7" exit /b %errorlevel% :identity_local rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI. -python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" +rem Локальный выпуск использует ту же проверенную установку Python, что prebuild. +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" exit /b %errorlevel% :usage echo Usage: postcompile.bat --identity "compiler-root" "build-dir" "artifact-name" "generated-metadata.json" "output-dir" ["export-dir"] 1>&2 diff --git a/tools/firmware-build/ccs/precompile.bat b/tools/firmware-build/ccs/precompile.bat index b45ea9a..0224c2e 100644 --- a/tools/firmware-build/ccs/precompile.bat +++ b/tools/firmware-build/ccs/precompile.bat @@ -24,7 +24,8 @@ if not exist "%~f3" ( echo [firmware-build] Edit product, target and version in this file, then rebuild. ) rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI. -python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" prepare --project "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4" +rem Общая обвязка находит Python 3.8+ или готовит локальный runtime до резервирования номера сборки. +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" prepare --project "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4" exit /b %errorlevel% :usage echo Usage: precompile.bat --identity "project-root" "firmware_identity.json" "generated-dir" 1>&2 diff --git a/tools/firmware-build/keil/firmware_identity.example.json b/tools/firmware-build/keil/firmware_identity.example.json index cf7b225..dc92096 100644 --- a/tools/firmware-build/keil/firmware_identity.example.json +++ b/tools/firmware-build/keil/firmware_identity.example.json @@ -2,5 +2,22 @@ "product": "MY_CONTROLLER", "target": "STM32F407", "version": [0, 1, 0], - "increment_version": true + "increment_version": true, + "flashing": { + "default_loader": "set-can", + "profiles": { + "set-can": { + "bitrate": 250000, + "node_id": 13, + "product_type": 358, + "hardware_min": 1, + "hardware_max": 1, + "base_address": 134230016 + }, + "set-rs485": { + "baudrate": 115200, + "base_address": 134230016 + } + } + } } diff --git a/tools/firmware-build/keil/postcompile.bat b/tools/firmware-build/keil/postcompile.bat index 81685fa..a331025 100644 --- a/tools/firmware-build/keil/postcompile.bat +++ b/tools/firmware-build/keil/postcompile.bat @@ -18,15 +18,19 @@ if /i not "%~x2"==".hex" if /i not "%~x2"==".bin" ( echo [firmware-build] Expected a ready HEX or BIN image: "%~2" 1>&2 exit /b 2 ) -python "%~dp0..\..\..\c\firmware-info\tools\identity.py" seal --image "%~f2" --metadata "%~f3" +rem Печать паспорта выполняется найденным или автоматически подготовленным Python. +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" seal --image "%~f2" --metadata "%~f3" if errorlevel 1 exit /b %errorlevel% -python "%~dp0..\..\..\c\firmware-info\tools\identity.py" verify --image "%~f2" --manifest "%~f2.manifest.json" +rem Проверка хеша использует общий runtime вместо случайного python.exe в PATH. +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" verify --image "%~f2" --manifest "%~f2.manifest.json" if errorlevel 1 exit /b %errorlevel% if "%~4"=="" goto archive_local -python "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3" --export "%~f4" +rem Экспорт архива получает те же аргументы и найденный интерпретатор, что остальные стадии. +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3" --export "%~f4" goto archive_done :archive_local -python "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3" +rem Архивирование без дополнительной папки также не зависит от глобальной настройки Python. +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3" :archive_done if errorlevel 1 exit /b %errorlevel% echo [firmware-build] Image: "%~f2" diff --git a/tools/firmware-build/keil/precompile.bat b/tools/firmware-build/keil/precompile.bat index 30eba48..1786952 100644 --- a/tools/firmware-build/keil/precompile.bat +++ b/tools/firmware-build/keil/precompile.bat @@ -16,7 +16,8 @@ if "%~2"=="" goto usage if "%~3"=="" goto usage if "%~4"=="" goto usage if not "%~5"=="" goto usage -python "%~dp0..\..\..\c\firmware-info\tools\identity.py" prepare --repository "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4" +rem Подготовка Python выполняется общей обвязкой без зависимости от PATH запущенного Keil. +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" prepare --repository "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4" exit /b %errorlevel% :usage echo Usage: precompile.bat --identity "project-root" "firmware_identity.json" "generated-dir" 1>&2 diff --git a/tools/firmware-build/python-runtime.ps1 b/tools/firmware-build/python-runtime.ps1 new file mode 100644 index 0000000..1cd42e1 --- /dev/null +++ b/tools/firmware-build/python-runtime.ps1 @@ -0,0 +1,87 @@ +[CmdletBinding(PositionalBinding = $false)] # Именованные служебные параметры не должны поглощать позиционный путь запускаемого Python-скрипта. +param( # Единая точка входа CCS/Keil работает без зависимости от python.exe в PATH. + [string]$ResolveFile, # При настройке IDE записывает точный путь проверенного интерпретатора в UTF-8. + [switch]$CheckOnly, # Проверка окружения не скачивает runtime и не запускает проектный скрипт. + [Parameter(Position = 0, ValueFromRemainingArguments = $true)][string[]]$PythonArguments # Путь скрипта и все оставшиеся аргументы передаются Python без позиционной привязки к ResolveFile. +) # Завершает описание публичных параметров обвязки. +$ErrorActionPreference = 'Stop' # Любая ошибка подготовки возвращается IDE как отказ шага сборки. +$env:PYTHONUTF8 = '1' # Python 3.8+ передаёт пути и диагностику в UTF-8 независимо от локальной кодовой страницы Windows. +[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false) # Windows PowerShell 5 должен читать вывод Python в той же кодировке. +$OutputEncoding = [Console]::OutputEncoding # Согласует кодировку дочерних native-процессов с JSON и сообщениями SETGUI. +function Test-FirmwarePython([string]$Candidate) { # Проверяет настоящий Python вместо одноимённого псевдонима Microsoft Store. + if (-not $Candidate -or -not (Test-Path -LiteralPath $Candidate -PathType Leaf) -or $Candidate -like '*WindowsApps*') { return $null } # Пропускает отсутствующие файлы и псевдонимы магазина без открытия окна установки. + try { # Неподходящий кандидат не мешает проверить остальные установленные интерпретаторы. + $result = & $Candidate -c 'import sys,json,hashlib,struct,tempfile,ctypes,xml.etree.ElementTree;sys.exit(1) if sys.version_info<(3,8) else print(sys.executable)' 2>$null # Общим инструментам нужны Python 3.8+ и только стандартная библиотека. + if ($LASTEXITCODE -eq 0 -and $result) { return [string]($result | Select-Object -Last 1) } # Возвращает фактический exe, а не имя команды или путь launcher. + } catch {} # Ошибка запуска одного кандидата оставляет поиск остальных доступным. + return $null # Отсутствие пригодного runtime обрабатывается общей установкой ниже. +} # Завершает проверку одного интерпретатора. +function Find-FirmwarePython { # Ищет готовый runtime до любых сетевых действий. + if ($env:FIRMWARE_PYTHON) { # Явный выбор администратора проекта имеет приоритет над автоматическим поиском. + $chosen = Test-FirmwarePython $env:FIRMWARE_PYTHON # Проверяет указанный абсолютный путь тем же набором импортов. + if (-not $chosen) { throw 'FIRMWARE_PYTHON должен указывать на работающий Python 3.8+.' } # Неверное явное значение не подменяется другой версией незаметно. + return $chosen # Оба шага IDE используют выбранный интерпретатор. + } # После явной настройки остальные кандидаты не нужны. + $cache = Join-Path $env:LOCALAPPDATA 'SET/FirmwareBuild' # Локальный runtime принадлежит пользователю и не требует администратора. + foreach ($relative in @('python-3.13.12-amd64/python.exe', 'python-3.8.10-win32/python.exe')) { # Порядок предпочитает современный runtime, сохраняя совместимость с Windows 7. + $chosen = Test-FirmwarePython (Join-Path $cache $relative) # Проверяет ранее подготовленную локальную копию. + if ($chosen) { return $chosen } # Повторные сборки обходятся без скачивания и установки. + } # Завершает поиск локального управляемого runtime. + foreach ($command in @('python', 'python3')) { # Установленный Python можно использовать без изменения пользовательского PATH. + $candidate = Get-Command $command -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 # Получает полный путь исполняемого файла команды. + if ($candidate) { $chosen = Test-FirmwarePython $candidate.Source; if ($chosen) { return $chosen } } # Не принимает неподходящую версию за готовое окружение. + } # После PATH проверяет зарегистрированные установки без требования перезапуска IDE. + foreach ($registry in @('HKCU:/Software/Python/PythonCore', 'HKLM:/Software/Python/PythonCore', 'HKLM:/Software/WOW6432Node/Python/PythonCore')) { # PEP 514 учитывает установки текущего пользователя, x64 и x86. + foreach ($version in @(Get-ChildItem -LiteralPath $registry -ErrorAction SilentlyContinue | Sort-Object PSChildName -Descending)) { # Перебирает только зарегистрированные версии Python. + $key = Get-Item -LiteralPath ($version.PSPath + '/InstallPath') -ErrorAction SilentlyContinue # Реестр хранит корневой каталог независимой установки. + if ($key) { $chosen = Test-FirmwarePython (Join-Path ([string]$key.GetValue('')) 'python.exe'); if ($chosen) { return $chosen } } # Возвращает первую реально работающую версию 3.8+. + } # Завершает перебор версий одного раздела реестра. + } # Ни один существующий runtime не оказался пригодным. + return $null # Вызывающий подготовит переносимый runtime, если проверка допускает установку. +} # Завершает поиск установленного Python. +try { # Все ошибки возвращаются одним ненулевым кодом, понятным CCS/Keil. + $pythonExe = Find-FirmwarePython # Использует готовую установку без изменения окружения машины. + if (-not $pythonExe -and $CheckOnly) { throw 'Python 3.8+ не найден; примените автоматическую настройку проекта для подготовки runtime.' } # Режим диагностики не устанавливает ПО. + if (-not $pythonExe) { # Установка нужна только компьютеру без подходящего Python. + $legacy = [Environment]::OSVersion.Version -lt [Version]'6.2' -or -not [Environment]::Is64BitOperatingSystem # Windows 7 и x86 используют последнюю совместимую бинарную ветку 3.8. + $version = if ($legacy) { '3.8.10' } else { '3.13.12' } # Зафиксированные версии делают подготовку воспроизводимой. + $arch = if ($legacy) { 'win32' } else { 'amd64' } # Разрядность runtime не зависит от разрядности установленной IDE. + $expected = if ($legacy) { '760dc79bcb434ee80b1001a30bb6f798287881851bac6d8137867894d40ef1fc' } else { '76f238f606250c87c6beac75dccd35ee99070a13490555936abb6cb64ecce3d0' } # SHA-256 официальных embeddable ZIP с python.org проверен при подготовке обвязки. + $cache = Join-Path $env:LOCALAPPDATA 'SET/FirmwareBuild' # Runtime хранится отдельно от исходников и сохраняется после Clean. + $destination = Join-Path $cache ('python-' + $version + '-' + $arch) # Каждый вариант получает независимый каталог пользователя. + New-Item -ItemType Directory -Force -Path $cache | Out-Null # Создаёт только собственный каталог инструментов сборки. + $temporary = Join-Path $cache ('download-' + [Guid]::NewGuid().ToString('N')) # Уникальный staging исключает частично установленный runtime. + New-Item -ItemType Directory -Path $temporary | Out-Null # Распаковка выполняется вне постоянного каталога. + try { # Ошибка сети или распаковки не должна повредить ранее установленный Python. + $archive = Join-Path $temporary 'python.zip' # Собственный архив удаляется вместе со staging. + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Загрузка использует TLS 1.2 также на Windows 7 с обновлённой .NET. + $client = New-Object Net.WebClient # WebClient доступен штатной Windows PowerShell без внешних модулей. + try { $client.DownloadFile(('https://www.python.org/ftp/python/' + $version + '/python-' + $version + '-embed-' + $arch + '.zip'), $archive) } finally { $client.Dispose() } # Скачивает строго официальный зафиксированный пакет Python. + $sha = [Security.Cryptography.SHA256]::Create() # Проверяет полные байты архива до запуска любого файла из него. + try { $actual = ([BitConverter]::ToString($sha.ComputeHash([IO.File]::ReadAllBytes($archive)))).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } # Нормализует SHA-256 для точного сравнения с закреплённым значением. + if ($actual -ne $expected) { throw 'SHA-256 архива Python не совпал с ожидаемым значением.' } # Неподтверждённый архив никогда не распаковывается. + Add-Type -AssemblyName System.IO.Compression.FileSystem # Использует штатную .NET 4.5+ для ZIP без зависимости от Python. + $unpacked = Join-Path $temporary 'runtime' # Отделяет исполняемые файлы от скачанного ZIP. + [IO.Compression.ZipFile]::ExtractToDirectory($archive, $unpacked) # Распаковывает проверенный официальный ZIP во временный каталог. + $checked = Test-FirmwarePython (Join-Path $unpacked 'python.exe') # До публикации проверяет версию и необходимые модули runtime. + if (-not $checked) { throw 'Подготовленный Python не запускается; проверьте Universal C Runtime Windows.' } # Отсутствующая системная зависимость не выдаётся за успех установки. + if (Test-Path -LiteralPath $destination) { throw ('Каталог runtime занят непригодной установкой: ' + $destination) } # Не удаляет существующие пользовательские файлы автоматически. + Move-Item -LiteralPath $unpacked -Destination $destination # Атомарно публикует только полностью проверенную локальную установку. + $pythonExe = Join-Path $destination 'python.exe' # Все последующие вызовы используют точный путь установленного runtime. + } finally { # Очищает только собственный staging после проверки его абсолютного расположения. + $resolved = [IO.Path]::GetFullPath($temporary) # Нормализует путь перед рекурсивным удалением на Windows. + if ($resolved.StartsWith([IO.Path]::GetFullPath($cache) + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { Remove-Item -LiteralPath $resolved -Recurse -Force -ErrorAction SilentlyContinue } # Удаляет исключительно созданную текущим вызовом папку внутри собственного кэша. + } # Завершает подготовку одного переносимого runtime. + } # Готовый Python найден или успешно подготовлен. + if ($ResolveFile) { [IO.File]::WriteAllText([IO.Path]::GetFullPath($ResolveFile), $pythonExe, (New-Object Text.UTF8Encoding($false))) } # GUI получает точный путь без разбора локализованного вывода консоли. + if ($PythonArguments.Count) { # Непустые аргументы означают запуск шага сборки после подготовки зависимостей. + $bootstrap = 'import os,runpy,sys;p=sys.argv[1];sys.argv=sys.argv[1:];sys.path.insert(0,os.path.dirname(os.path.abspath(p)));runpy.run_path(p,run_name=''__main__'')' # Одинарные Python-кавычки сохраняются при передаче через Windows PowerShell 5 и изолированный runtime. + & $pythonExe -c $bootstrap @PythonArguments # Передаёт исходные аргументы отдельными элементами без повторного разбора командной оболочкой. + exit $LASTEXITCODE # Ошибка Python останавливает prebuild/postbuild с исходным кодом завершения. + } # В режиме настройки достаточно успешной подготовки runtime. + Write-Output $pythonExe # Терминал показывает фактически выбранный интерпретатор. + exit 0 # Успех означает работающий Python с проверенной стандартной библиотекой. +} catch { # Единая диагностика доступна как в IDE, так и в окне настройки SETGUI. + [Console]::Error.WriteLine('[firmware-build] ' + $_.Exception.Message) # Показывает конкретную причину отказа без скрытого перехода к другой цепочке. + exit 2 # Ненулевой код запрещает дальнейшие стадии неподготовленной сборки. +} # Завершает общий запуск runtime и инструментов прошивки.