Обеспечь единый процесс обновления для STM32F103/F407/G474 и TMS320F2812/F28335: discovery, служебные ответы, BUSY, тайм-аут сессии и подтверждение приложения. Добавь самостоятельные проекты CCS, аппаратные хранилища и Python-клиент. Совместимость: структуры конфигурации и порта расширены, поэтому потребители должны пересобраться и обнулить новые поля. Для C28x callbacks используют pcan_boot_byte_t; периодический pcan_boot_poll обслуживает discovery и настроенный тайм-аут.
133 lines
24 KiB
Python
133 lines
24 KiB
Python
"""Сборка отдельных 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.
|