Files
templates/c/protocan-boot/projects/build.py
T
Andrey 41e24629ce Расширить DEIF, загрузку CAN и общую обработку сигналов
Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt.
Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor.
Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов.
Обновить справочники устройств, проекты CCS и регрессионные проверки.
2026-10-24 15:40:51 +03:00

87 lines
15 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.
import hashlib # Связывает собранный OUT с контрольной суммой в локальном build manifest.
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 build(device, configuration, compiler, output=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) # Генерирует результаты только в каталоге выбранной сборки.
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.
checked = verify(device, image, compiler) # Физические LOAD-адреса, boot branch и C entry проверяются после каждой CLI-сборки.
manifest = {"device": device, "configuration": configuration, "sha256": hashlib.sha256(image.read_bytes()).hexdigest(), "sources": {str(path.relative_to(ROOT)): hashlib.sha256(path.read_bytes()).hexdigest() for path in sources}, **checked} # Связывает OUT с конкретными исходниками и результатом проверки карты.
image.with_suffix(".out.manifest.json").write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") # Не изменяет версии приложения или данные физической памяти.
return image # Вызывающий явно решает, когда устанавливать OUT через JTAG.
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-проекта.
args = parser.parse_args(); print(build(args.device, args.configuration, args.compiler, args.output)) # Сообщает конкретный успешно проверенный OUT, не инициируя JTAG.