Files
templates/python/protocan/f28335_flash.py
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

100 lines
16 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.
import base64 # Декодирует загружаемые секции из официального XML TI OFD, сохраняя адреса LOAD и RUN.
from pathlib import Path # Сохраняет изменения только в явно переданном результате текущей сборки.
import struct # Упаковывает адресуемые 16-битные слова C28x младшей октетой вперёд.
import subprocess # Вызывает локальный инспектор объектных файлов без доступа к JTAG.
import xml.etree.ElementTree as ET # Разбирает фактические секции COFF независимо от соседней карты линкера.
import zlib # CRC32 IEEE совпадает с проверкой постоянного загрузчика и учитывает незаписанные слова 0xFFFF.
APP_START = 0x300000 # Сектор H начинает область приложения; аппаратный boot остаётся в секторе A.
HEADER_WORDS = 16 # Заголовок версии 1 занимает 16 слов C28x, то есть 32 физических октета.
APP_DATA = APP_START + HEADER_WORDS # Исполняемые секции следуют после служебного заголовка приложения.
APP_STOP = 0x338000 # Исключённая верхняя граница совпадает с началом защищённого сектора A.
def descriptor(entry, loaded): # Создаёт проверяемое описание Flash-приложения без сектора постоянного загрузчика.
if not loaded or any(not APP_START <= address < APP_STOP for address in loaded): # Любое слово в секторе A, CSM, OTP или RAM делает образ непригодным для обновления приложения.
raise ValueError('Flash-приложение пересекает защищённый загрузчик или выходит за область 0x300000–0x337FFF') # Отказ происходит до стирания физической памяти.
end = max(loaded) + 1 # Верхняя граница включает все Flash LOAD-секции, в том числе копируемый в RAM код API.
if not APP_DATA <= entry < end or entry not in loaded: # Точка запуска обязана указывать на действительно присутствующее слово кода после заголовка.
raise ValueError('Точка входа Flash-приложения отсутствует в загружаемом коде') # Исключает пустой заголовок и устаревшую RAM-сборку.
payload = struct.pack('<' + 'H' * (end - APP_DATA), *(loaded.get(address, 0xFFFF) for address in range(APP_DATA, end))) # Пробелы между секциями должны оставаться стёртыми; порядок октетов соответствует C28x.
crc = zlib.crc32(payload) # Контрольная сумма защищает весь непрерывный диапазон приложения, включая промежутки.
values = (0x4146, 0x3142, 1, HEADER_WORDS, entry & 65535, entry >> 16, APP_DATA & 65535, APP_DATA >> 16, end & 65535, end >> 16, crc & 65535, crc >> 16, (~entry) & 65535, ((~entry) >> 16) & 65535, 0x5AA5, 0xA55A) # Первые четыре физических октета образуют ASCII FAB1; далее следуют версия, размер, адреса, CRC, инверсия entry и маркеры завершения.
return struct.pack('<16H', *values) # Передаёт ровно 32 октета без платформенного выравнивания структуры.
def validate_words(entry, loaded): # Одинаково проверяет содержимое JTAG COFF и транспортной таблицы RS Flash.
if any(APP_START + index not in loaded for index in range(HEADER_WORDS)): # Все слова заголовка должны присутствовать явно, а не получаться заполнением 0xFFFF.
raise ValueError('Нет полного заголовка Flash-приложения FAB1; требуется новая сборка под постоянный загрузчик') # Старый образ с codestart по 0x33FFF6 не допускается.
actual = struct.pack('<16H', *(loaded[APP_START + index] for index in range(HEADER_WORDS))) # Получает описание именно из передаваемых данных.
if actual != descriptor(entry, loaded): # Проверяет CRC, границы, обе половины entry и маркеры одной независимой реконструкцией.
raise ValueError('Заголовок или CRC32 Flash-приложения FAB1 не соответствует данным') # Повреждение нельзя скрыть корректным именем файла или внешним паспортом.
def parse_sci8(data): # Разбирает только приложение ниже сектора A, не разрешая аппаратный переход ROM.
if len(data) < 24 or len(data) % 2 or data[:2] != b'\xaa\x08': # SCI8 имеет ключ 0x08AA, 22-октетный заголовок и чётный терминатор.
raise ValueError('Неверный заголовок или размер Flash SCI8') # Обычный бинарный дамп и COFF не являются таблицей адресных блоков TI.
def address(offset): # Адрес TI состоит из старшего 16-битного слова, затем младшего; каждое слово little endian.
high, low = struct.unpack_from('<HH', data, offset) # Читает две половины без зависимости от архитектуры Python.
return (high << 16) | low # Возвращает адрес в словах C28x, а не байтовое смещение.
entry, offset, loaded, blocks = address(18), 22, {}, [] # Хранит точку запуска, позицию таблицы и неперекрывающиеся диапазоны.
while offset + 2 <= len(data): # Длина блока должна присутствовать полностью даже для завершающего нуля.
count, = struct.unpack_from('<H', data, offset) # Размер полезных данных выражен 16-битными словами.
offset += 2 # Переходит к адресу блока либо к концу таблицы.
if not count: # Нулевой размер завершает адресную таблицу без последующего поля адреса.
if offset != len(data): # Дополнительные байты после терминатора недопустимы даже при правильной CRC полезной части.
raise ValueError('Хвост после терминатора Flash SCI8') # Не принимает склеенные или частично заменённые файлы.
validate_words(entry, loaded) # До передачи проверяет CRC и отсутствие слов постоянного загрузчика.
return entry, loaded, blocks # Вызывающий использует проверенные блоки для записи либо сравнения с COFF.
if offset + 4 + 2 * count > len(data): # Адрес и все слова должны помещаться в текущем файле.
raise ValueError('Обрезанный блок Flash SCI8') # Не читает случайные байты за границей изображения.
start = address(offset) # Получает абсолютный адрес назначения очередного блока.
offset += 4 # Пропускает две половины абсолютного адреса.
if not APP_START <= start < start + count <= APP_STOP: # Сектор A и весь аппаратный boot исключены независимо от имени секции.
raise ValueError(f'Запрещённый Flash-диапазон 0x{start:06X}, {count} слов: загрузчик защищён') # Показывает точное пересечение до начала обновления.
values = struct.unpack_from('<' + 'H' * count, data, offset) # Читает только полностью проверенный блок.
if any(start + index in loaded for index in range(count)): # Дублирование адреса могло бы скрыть подмену заголовка поздним блоком.
raise ValueError('Перекрывающиеся блоки Flash SCI8') # Запрещает неоднозначный порядок программирования.
loaded.update((start + index, value) for index, value in enumerate(values)) # Сохраняет каждое слово по его физическому LOAD-адресу.
blocks.append((start, data[offset:offset + count * 2])) # Сохраняет исходные октеты без изменения порядка секций.
offset += count * 2 # Переходит к размеру следующего блока.
raise ValueError('Нет терминатора Flash SCI8') # Обрезанный файл не считается полным приложением.
def coff_sections(document): # Извлекает адреса и точные октеты из результата TI ofd2000 -x.
coff = document.find('.//ti_coff') # Рабочая прошивка F28335 использует COFF ABI.
if coff is None or coff.findtext('file_header/target_id') != '0x9d' or coff.findtext('file_header/exec') != 'true': # Проверяет процессор и признак связанного исполняемого файла.
raise ValueError('Требуется исполняемый COFF C2000') # Объектные модули и файлы другой архитектуры не программируются.
entry = int(coff.findtext('optional_file_header/entry_point', '0'), 0) # Entry задаёт запуск приложения через общий Flash boot.
sections, loaded = {}, {} # Отдельно хранит XML полей секций и адресное пространство программируемых слов.
for section in coff.findall('section'): # Рассматривает все секции, включая LOAD для функций, исполняемых из RAM.
sections[section.findtext('name')] = section # Служебный заголовок находится по стабильному имени .flash_app.
raw, start = section.findtext('raw_data'), int(section.findtext('virtual_addr', '0'), 0) # OFD называет virtual адресом LOAD, physical — RUN.
if raw is None or section.findtext('copy') == 'true': # Только явно незагружаемые COPY/отладочные секции и секции без данных исключаются из программирования.
continue # Их наличие не расширяет область разрешённого программирования.
payload = base64.b64decode(raw) # Получает точные октеты вместо доверия к соседней MAP.
if len(payload) != int(section.findtext('raw_data_size', '0'), 0) or len(payload) % 2 or not APP_START <= start < start + len(payload) // 2 <= APP_STOP: # Любая загружаемая секция обязана целиком находиться ниже A; RAM, OTP, CSM и несогласованный размер запрещены.
raise ValueError('COFF содержит запрещённую Flash-секцию: ' + str(section.findtext('name'))) # Старый самостоятельный codestart будет отклонён.
for index, value in enumerate(struct.unpack('<' + 'H' * (len(payload) // 2), payload)): # Восстанавливает 16-битное представление каждого слова C28x.
if start + index in loaded: # Две секции не должны программировать одно физическое слово.
raise ValueError('Перекрывающиеся LOAD-секции COFF') # Сохраняет единственность заголовка и однозначную контрольную сумму.
loaded[start + index] = value # Собирает ожидаемый Flash-образ для проверки CRC и конверсии SCI8.
return entry, loaded, sections # Метаданные нужны как подписанию, так и независимой проверке готового OUT.
def seal_coff(image, inspector): # Заполняет только заранее выделенный linker заголовок после успешной линковки.
image = Path(image) # Принимает путь текущей сборки без зависимости от рабочего каталога.
document = ET.fromstring(subprocess.check_output([str(inspector), '-x', str(image)])) # Доверяет размещению секций, подтверждённому официальным TI-инспектором.
entry, loaded, sections = coff_sections(document) # Отсекает любой LOAD в сектор A до изменения двоичного файла.
section = sections.get('.flash_app') # Секция размером 16 слов резервируется linker и не меняет адресов остальной программы.
if section is None or int(section.findtext('virtual_addr', '0'), 0) != APP_START or int(section.findtext('raw_data_size', '0'), 0) != HEADER_WORDS * 2: # Нельзя добавлять заголовок поверх машинного кода.
raise ValueError('Linker должен выделить .flash_app из 16 слов по адресу 0x300000') # Ошибка требует пересборки вместо небезопасного смещения программы.
offset = int(section.findtext('file_offsets/raw_data_ptr', '0'), 0) # OFD указывает точное файловое смещение содержимого секции COFF.
raw, signed = bytearray(image.read_bytes()), descriptor(entry, loaded) # Считает CRC уже окончательно связанных Flash LOAD-секций.
if offset <= 0 or offset + len(signed) > len(raw): # Проверяет размер файла до адресной записи заголовка.
raise ValueError('Неверное смещение заголовка COFF') # Повреждённые метаданные не могут изменить постороннюю часть OUT.
raw[offset:offset + len(signed)] = signed # Меняет только зарезервированные 32 октета, не трогая символы, адреса и отладочную информацию.
image.write_bytes(raw) # Сохраняет подписанный результат до конверсии в SCI8 и создания внешнего SHA-256.
loaded.update((APP_START + index, value) for index, value in enumerate(struct.unpack('<16H', signed))) # Приводит адресную модель к записанному заголовку.
validate_words(entry, loaded) # Независимо проверяет только что сформированный результат.