Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt. Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor. Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов. Обновить справочники устройств, проекты CCS и регрессионные проверки.
87 lines
15 KiB
Python
87 lines
15 KiB
Python
"""Сборка отдельных 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.
|