Files
templates/c/protocan-boot/projects/build.py
T
Andrey c6390ae1c9 feat(protocan-boot): Добавь порты MCU и сервисы загрузки
Обеспечь единый процесс обновления для STM32F103/F407/G474 и TMS320F2812/F28335: discovery, служебные ответы, BUSY, тайм-аут сессии и подтверждение приложения. Добавь самостоятельные проекты CCS, аппаратные хранилища и Python-клиент.

Совместимость: структуры конфигурации и порта расширены, поэтому потребители должны пересобраться и обнулить новые поля. Для C28x callbacks используют pcan_boot_byte_t; периодический pcan_boot_poll обслуживает discovery и настроенный тайм-аут.
2026-10-23 14:16:19 +03:00

133 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Сборка отдельных CCS-проектов ProtoCAN для F28335/M95M02 и F2812/AM29LV800BT.""" # Требует только Python и установленный TI C2000 COFF compiler.
import argparse # Разбирает выбранный контроллер, конфигурацию и пути toolchain.
import base64 # Читает настоящие программируемые данные COFF из официального OFD XML.
from datetime import datetime, timezone # Фиксирует момент prebuild в UTC; имя архива общий модуль переводит в UTC+3.
import hashlib # Связывает собранный OUT с контрольной суммой в локальном build manifest.
import importlib.util # Подключает общий экспорт firmware-info без копирования его алгоритма архивирования.
import json # Сохраняет точные команды, происхождение исходников и результаты проверки.
from pathlib import Path # Разрешает все зависимости внутри templates относительно расположения проекта.
import subprocess # Вызывает установленный TI compiler без shell-интерполяции аргументов.
import xml.etree.ElementTree as ET # Использует действующие .project/.cproject как источник состава и опций сборки.
ROOT = Path(__file__).resolve().parents[1] # Корень protocan-boot содержит codec, порты, проекты и vendored TI support.
DEFAULT_COMPILER = Path("C:/ti/ccs1230/ccs/tools/compiler/ti-cgt-c2000_22.6.0.LTS") # Сохраняет проверенную версию установленного C2000 COFF compiler.
def settings(device, configuration, compiler): # Читает те же исходники и пути, которые получает импортированный проект CCS.
project = ROOT / "projects/ccs" / device # Каждое семейство контроллера имеет собственный Eclipse projectDescription.
config = next(item for item in ET.parse(project / ".cproject").findall(".//configuration") if item.get("name") == configuration) # Не подменяет Debug настройками Release или другого устройства.
options = {item.get("superClass", "").rsplit(".", 1)[-1]: item for item in config.findall("folderInfo/toolChain/tool/option")} # Собирает только параметры общего folderInfo без переопределений отдельных файлов.
macros = {"${PROJECT_ROOT}": str(project), "${PROJECT_LOC}": str(project), "${CG_TOOL_ROOT}": str(compiler)} # Разрешает только известные пути, не выполняя shell-подстановки.
def values(name): # Возвращает listOptionValue после подстановки локальных макросов CCS.
result = [] # Порядок include paths и библиотек остаётся тем же, что в проекте.
for item in options[name].findall("listOptionValue"): # Обрабатывает каждое явное значение текущей конфигурации.
value = item.get("value", "") # Отсутствующее значение не превращается в имя неизвестного каталога.
for macro, replacement in macros.items(): value = value.replace(macro, replacement) # Ограничивает подстановки определёнными выше путями.
if "${" in value: raise ValueError("Unresolved CCS macro: " + value) # Останавливает сборку вместо поиска случайной зависимости на машине.
result.append(value) # Добавляет параметр в исходном порядке конфигурации CCS.
return result # Возвращает обычные аргументы без скрытых изменений окружения.
sources = sorted(project.glob("*.c")) + sorted(project.glob("*.asm")) # Локальные production-исходники проекта компилируются вместе с аппаратным портом.
commands = sorted(project.glob("*.cmd")) # Каждый проект имеет собственную разметку памяти соответствующего F28xx.
for link in ET.parse(project / ".project").findall(".//linkedResources/link"): # Добавляет только explicit linkedResources из templates.
uri = link.findtext("locationURI", "") # Все общие исходники привязаны к переменной PCAN_ROOT.
if not uri.startswith("PCAN_ROOT/"): raise ValueError("Unsupported linked resource: " + uri) # Не допускает зависимость от старого firmware checkout или абсолютного чужого пути.
path = ROOT / uri.removeprefix("PCAN_ROOT/") # Разрешает vendor и общие исходники внутри переносимого protocan-boot.
if path.suffix.lower() in (".c", ".asm"): sources.append(path) # Заголовки не компилируются самостоятельными единицами трансляции.
elif path.suffix.lower() == ".cmd": commands.append(path) # Подключает оригинальное TI размещение регистров выбранного контроллера.
flags = ["-v28", "-ml", "-mt", "--abi=coffabi", "-O" + options["OPT_LEVEL"].get("value").rsplit(".", 1)[-1], "--opt_for_speed=" + options["OPT_FOR_SPEED"].get("value").rsplit(".", 1)[-1], "--gen_func_subsections=on"] # Повторяет архитектуру COFF, большие адреса, оптимизацию и сборку отдельных функций проекта CCS.
if device == "f28335": flags.append("--float_support=fpu32") # Только F28335 имеет используемый аппаратный FPU32.
flags += ["--include_path=" + value for value in values("INCLUDE_PATH")] + ["--define=" + value for value in values("DEFINE")] # Передаёт конкретные аппаратные заголовки и profile defines без потери различий между контроллерами.
if configuration == "Debug": flags.append("-g") # Debug OUT сохраняет символы для пошагового JTAG-анализа загрузчика.
return project, config.get("artifactName"), flags, sources, commands, values("LIBRARY"), values("SEARCH_PATH") # Возвращает полный воспроизводимый набор исходников и linker inputs.
def verify(device, image, compiler): # Проверяет реальные секции OUT после линковки, а не только текст соседнего MAP.
xml = ET.fromstring(subprocess.check_output([str(compiler / "bin/ofd2000.exe"), "-x", str(image)])) # Официальный декодер TI читает COFF текущей завершившейся сборки.
coff = xml.find(".//ti_coff") # Загружать через JTAG разрешено именно executable C2000 COFF.
if coff is None or coff.findtext("file_header/exec") != "true" or coff.findtext("file_header/target_id") != "0x9d": raise ValueError("Expected executable C2000 COFF") # Не подписывает чужой объект, ELF либо незалинкованный файл.
first, end, boot = (0x338000, 0x33FF80, 0x33FFF6) if device == "f28335" else (0x3F6000, 0x3F7F80, 0x3F7FF6) # Адреса внутренних секторов A и аппаратных boot-переходов различаются между F28335/F2812.
loaded = set() # Повторная загрузка одного физического слова из двух секций запрещена.
for section in coff.findall("section"): # Рассматривает только реальные данные секций после линковки.
address = int(section.findtext("virtual_addr", "0"), 0); raw = section.findtext("raw_data") # COFF virtual_addr обозначает LOAD, physical_addr — RUN копируемых ramfuncs.
if raw is None or section.findtext("copy") == "true" or section.findtext("name") == ".adc_cal": continue # Исключает только явные COPY/отладочные секции, BSS без raw_data и заводскую ADC-калибровку; загружаемый RAM-код обязан иметь Flash LOAD.
data = base64.b64decode(raw) # Читает точные октеты программируемого содержимого из COFF, а не считает по объявленному размеру.
if len(data) % 2 or not (first <= address < address + len(data) // 2 <= end or address == boot and len(data) == 4): raise ValueError("Loaded section outside bootloader Flash: " + str(section.findtext("name"))) # Защищает OTP, CSM, внешнюю NOR/EEPROM и остальные внутренние секторы.
for word in range(address, address + len(data) // 2): # Контролирует физические 16-битные слова всех программируемых секций.
if word in loaded: raise ValueError("Overlapping load sections") # Даже совпадающие данные не разрешают неоднозначную разметку OUT.
loaded.add(word) # Фиксирует фактически загружаемый диапазон текущего результата.
entry = int(coff.findtext("optional_file_header/entry_point", "0"), 0) # Извлекает реальную точку входа C runtime из заголовка файла.
if entry not in loaded or boot not in loaded or boot + 1 not in loaded: raise ValueError("Missing loaded C entry or codestart") # JTAG OUT обязан одновременно содержать C startup и аппаратный Flash boot переход.
return {"entry_word": entry, "boot_word": boot, "loaded_words": len(loaded)} # Эти проверенные значения входят в build manifest рядом с SHA-256.
def identity_tools(): # CCS и CLI используют общий формат имени папки, manifest и SHA-256 из templates.
spec = importlib.util.spec_from_file_location("canboot_identity", ROOT.parent / "firmware-info/tools/identity.py") # firmware-info расположен рядом с protocan-boot внутри каталога c.
module = importlib.util.module_from_spec(spec) # Загрузка не резервирует версию и не создаёт артефакты приложения 167.
spec.loader.exec_module(module) # Выполняет только объявления общего модуля подготовки и локального экспорта.
return module # Вызывающий получает единую реализацию архивирования без сетевой публикации.
def source_hashes(device): # Связывает prebuild и postbuild с исходниками выбранного загрузчика, включая включаемые C-файлы и заголовки.
folders = [ROOT / "include", ROOT / "src", ROOT / "ports" / ("tms320" + device), ROOT / "vendor/ti" / device, ROOT / "projects/ccs" / device] # Общий автомат разделяется, аппаратные файлы второго контроллера в паспорт не попадают.
files = {ROOT / "projects/build.py"} # Изменение самого процесса между подготовкой и копированием требует нового prebuild.
for folder in folders: # Проверяет production-файлы библиотеки, порта, TI support и текущего CCS-проекта.
files.update(path for path in folder.rglob("*") if path.is_file() and path.suffix.lower() in (".c", ".h", ".asm", ".cmd", ".inc") and not {"Debug", "Release", "generated", "firmware"}.intersection(path.relative_to(folder).parts[:-1])) # Исключает результаты сборки; адреса памяти и аппаратные defines входят через CMD/H.
files.update(ROOT / "projects/ccs" / device / name for name in (".project", ".cproject", ".ccsproject")) # Настройки ABI, include paths и список linkedResources тоже должны оставаться неизменными.
return {path.relative_to(ROOT).as_posix(): hashlib.sha256(path.read_bytes()).hexdigest() for path in sorted(files)} # SHA-256 относится к точным байтам каждого исходного файла до компиляции.
def prepare(device, configuration, compiler, output): # Prebuild фиксирует происхождение именно той конфигурации, которую затем линкует CCS либо CLI.
project, name, *_ = settings(device, configuration, compiler) # Проверяет наличие выбранного проекта и узнаёт штатное имя OUT.
identity = identity_tools(); source = identity.git_state(ROOT.parents[1]) # Корень templates содержит Git-ревизию самостоятельных CAN boot проектов.
metadata = {"schema": 1, "product": name, "target": "TMS320" + device.upper(), "device": device, "configuration": configuration, "execution": "FLASH", "image_format": "COFF", "purpose": "canboot", "built_at_utc": datetime.now(timezone.utc).replace(microsecond=0).isoformat(), "source": source, "sources": source_hashes(device)} # Внешний паспорт описывает постоянный загрузчик; версию и встроенный паспорт приложения не имитирует.
identity.write_atomic(output / "generated/canboot_build.json", json.dumps(metadata, ensure_ascii=False, indent=2) + "\n") # Отдельный BuildDirectory не позволяет Debug и Release перезаписать подготовку друг друга.
return metadata # Позволяет проверить подготовку без запуска компилятора или доступа к плате.
def postbuild(device, configuration, compiler, output, export=None): # Проверяет готовый OUT и копирует только текущий комплект в папку соответствующего контроллера SETGUI.
output = output.resolve(); project, name, *_ = settings(device, configuration, compiler) # Имена OUT/MAP берутся из активной конфигурации CCS, а не из поиска старых сборок.
metadata = json.loads((output / "generated/canboot_build.json").read_text(encoding="utf-8")) # Без успешного prebuild выпуск останавливается до изменения каталога GUI.
if metadata.get("device") != device or metadata.get("configuration") != configuration or metadata.get("sources") != source_hashes(device): # Не допускает паспорт другой конфигурации или изменения исходников после подготовки.
raise ValueError("CAN boot sources/configuration changed: run prebuild and rebuild") # Требует согласованную пересборку вместо копирования неподтверждённого результата.
image = output / (name + ".out"); map_path = image.with_suffix(".map") # Оба файла принадлежат одному штатному результату линковки.
if not map_path.is_file(): raise ValueError("Missing CAN boot MAP: " + str(map_path)) # Неполный комплект не попадает в папку установки через JTAG.
metadata.update(verify(device, image, compiler)) # Повторно проверяет COFF LOAD-адреса, C entry и boot branch перед каждым экспортом из CCS или CLI.
identity = identity_tools(); metadata_path = output / "canboot_build.json" # Сохраняет проверенный внешний паспорт отдельно от исходной записи prebuild.
identity.write_atomic(metadata_path, json.dumps(metadata, ensure_ascii=False, indent=2) + "\n") # Диапазоны Flash и SHA исходников доступны рядом с локальным результатом.
identity.seal(image, metadata_path) # Manifest и SHA-256 рядом с OUT подписывают точные байты файла для JTAG.
destination = export if export is not None else ROOT.parents[2] / "SETGUI/..periph_28335/firmware/periph28335/balzam167/canboot" / device.removeprefix("f") # Сохраняет ожидаемую структуру 167/canboot/28335 и 167/canboot/2812 относительно соседнего SETGUI.
result = identity.archive_build(image, metadata_path, output / "firmware", extra_files=[map_path], export=destination) # Общий архиватор переносит только OUT, MAP, manifest, SHA-256 и метаданные текущего выпуска.
identity.write_atomic(output / "firmware/latest_build.json", json.dumps(result, ensure_ascii=False, indent=2) + "\n") # Указывает точные локальную и скопированную папки последнего успешного выпуска.
return image # Совместимый CLI по-прежнему возвращает путь проверенного OUT для первоначальной установки через JTAG.
def build(device, configuration, compiler, output=None, export=None): # Полностью собирает выбранный проект и экспортирует проверенный загрузчик без установки на плату.
compiler = compiler.resolve(); project, name, flags, sources, commands, libraries, searches = settings(device, configuration, compiler) # Использует абсолютный toolchain и актуальную конфигурацию CCS.
output = (output or project / configuration).resolve(); output.mkdir(parents=True, exist_ok=True) # Генерирует результаты только в каталоге выбранной сборки.
prepare(device, configuration, compiler, output) # CLI выполняет тот же prebuild, который настроен в управляемом проекте CCS.
objects = [] # В линковку входят только успешно пересобранные исходники текущего вызова.
with (output / "build.log").open("w", encoding="utf-8") as log: # Журнал содержит полный набор команд и диагностику TI.
def run(command): # Не продолжает сборку после ошибки компилятора или linker.
log.write(json.dumps([str(arg) for arg in command], ensure_ascii=False) + "\n"); log.flush() # Сохраняет команду до запуска процесса без исполнения как shell-кода.
subprocess.run([str(arg) for arg in command], cwd=project, stdout=log, stderr=subprocess.STDOUT, check=True) # Ненулевой статус исключает публикацию ложного успешного результата.
for source in sources: # Все локальные и linked TI-файлы пересобираются для выбранного контроллера.
obj = output / (source.stem + ".obj"); run([compiler / "bin/cl2000.exe", *flags, "-c", "--output_file=" + str(obj), source]); objects.append(obj) # Объекты F2812 soft-float и F28335 FPU32 не смешиваются.
image = output / (name + ".out") # Имя результата совпадает с artifactName импортированного CCS-проекта.
link = [compiler / "bin/cl2000.exe", "-v28", "--abi=coffabi"] + (["--float_support=fpu32"] if device == "f28335" else []) # Linker ABI и FPU точно соответствуют скомпилированным объектам.
run([*link, "-z", "--rom_model", "--entry_point=_c_int00", "--stack_size=0x3f0", "--warn_sections", "--reread_libs", "-m" + str(image.with_suffix(".map")), "-o", image, *["--search_path=" + value for value in searches], *commands, *objects, *["-l" + value for value in libraries]]) # Подключает только собственный linker и оригинальные peripheral sections выбранного MCU.
return postbuild(device, configuration, compiler, output, export) # CCS и CLI проверяют и копируют готовый комплект единственной общей функцией.
if __name__ == "__main__": # Импорт для проверки проектов не запускает compiler автоматически.
parser = argparse.ArgumentParser(description=__doc__) # Описывает два самостоятельных проекта и отсутствие программирования платы.
parser.add_argument("--device", choices=("f2812", "f28335"), required=True) # Не допускает случайной сборки для другого контроллера.
parser.add_argument("--configuration", choices=("Debug", "Release"), default="Debug") # Повторяет имена управляемых конфигураций CCS.
parser.add_argument("--compiler", type=Path, default=DEFAULT_COMPILER) # Поддерживает другую установленную версию TI с COFF ABI.
parser.add_argument("--output", type=Path) # Разрешает изолированную проверочную сборку без загрязнения импортированного CCS-проекта.
parser.add_argument("--step", choices=("build", "prepare", "postbuild"), default="build") # CCS запускает подготовку до компиляции и экспорт после своей линковки; CLI по умолчанию выполняет весь цикл.
parser.add_argument("--export", type=Path) # Переопределяет корень архивов 28335 либо 2812 при другом расположении SETGUI.
args = parser.parse_args(); output = (args.output or ROOT / "projects/ccs" / args.device / args.configuration).resolve() # Явный BuildDirectory CCS и CLI используют одинаковый формат результата.
if args.step == "prepare": prepare(args.device, args.configuration, args.compiler, output); print(output / "generated/canboot_build.json") # Фиксирует подготовку и выводит короткий путь к её паспорту без длинного списка SHA исходников.
elif args.step == "postbuild": print(postbuild(args.device, args.configuration, args.compiler, output, args.export)) # Проверяет OUT/MAP активного BuildDirectory и экспортирует один комплект.
else: print(build(args.device, args.configuration, args.compiler, output, args.export)) # Полный CLI-цикл включает те же подготовку и копирование, что CCS.