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