Добавить генерацию карт COFF для TMS320F28335 и TMS320F2812, проверку CMD/MAP и русскую инструкцию. Проверки: 14 тестов, включая четыре реальные линковки TI для RAM и Flash.
318 lines
54 KiB
Python
318 lines
54 KiB
Python
"""Создаёт и проверяет стартовые карты COFF TMS320F28335/F2812; не заменяет TI linker.""" # Утилита работает офлайн, читает текстовые CMD/MAP и не обращается к устройству.
|
||
import argparse # Стандартная библиотека обеспечивает переносимый CLI без пакетов SDK.
|
||
from dataclasses import dataclass # Числовые записи отделяют слова C28x от физических октетов файла.
|
||
import json # Структурированный отчёт пригоден для CI без разбора русского текста.
|
||
from pathlib import Path # Пути не зависят от установленного SDK и рабочего каталога.
|
||
import re # Ограниченная грамматика не исполняет макросы или выражения пользовательского CMD.
|
||
import sys # Передаёт отдельные коды ошибок и неполной проверки вызывающей сборке.
|
||
|
||
@dataclass(frozen=True) # Адреса исходной карты остаются неизменяемыми при проверке.
|
||
class Region: # Один MEMORY-регион в 16-битных адресуемых словах C28x.
|
||
name: str # Название связывает регион с выходной секцией.
|
||
page: int # PAGE разделяет пространство линкера, но не физическую память.
|
||
origin: int # Первый адрес слова в интервале.
|
||
length: int # Число слов до исключённого правого конца.
|
||
|
||
DEVICES = { # Числа сверены с первичными TI datasheet и официальными linker-примерами, указанными в README.
|
||
'28335': {'flash': (0x300000, 0x340000), 'begin': 0x33FFF6, 'otp': (0x380400, 0x380800), 'rom': (0x3FE000, 0x400000), 'ram': ((0, 0x800), (0x8000, 0x10000)), 'ram_entry': 0, 'alias': (0x3F8000, 0x3FC000, 0x8000)}, # SPRS439Q: L0–L3 имеют высокое зеркало, Flash содержит 256K слов.
|
||
'2812': {'flash': (0x3D8000, 0x3F8000), 'begin': 0x3F7FF6, 'otp': (0x3D7800, 0x3D7C00), 'rom': (0x3FF000, 0x400000), 'ram': ((0, 0x800), (0x8000, 0xA000), (0x3F8000, 0x3FA000)), 'ram_entry': 0x3F8000, 'alias': None}, # SPRS174V: H0 является отдельной RAM8K слов; Flash128K слов.
|
||
} # Внешняя SRAM, EEPROM и Flash API зависят от платы и сюда не добавляются.
|
||
NUM = r'(?:0[xX][0-9A-Fa-f]+|[0-9]+)' # Принимает только буквальные значения без выполнения C-выражений.
|
||
NAME = r'[A-Za-z_.$][\w.$]*' # Обычные имена MEMORY/SECTIONS TI без расширенного GROUP-синтаксиса.
|
||
|
||
|
||
def issue(level, code, message): # Формирует единый формат машиночитаемой и русской диагностики.
|
||
return {'level': level, 'code': code, 'message': message} # incomplete влияет на код возврата, а не скрывается как справочный текст.
|
||
|
||
|
||
def clean_text(text): # Удаляет C-комментарии без изменения номеров строк.
|
||
return re.sub(r'/\*.*?\*/|//[^\n]*', lambda m: ''.join('\n' if c == '\n' else ' ' for c in m[0]), text, flags=re.S) # Скобки внутри пояснений не становятся языком linker-файла.
|
||
|
||
|
||
def body(text, keyword): # Выделяет MEMORY или SECTIONS по парным скобкам.
|
||
match = re.search(r'\b' + keyword + r'\s*\{', text) # Требует фактическую директиву с телом.
|
||
if not match: # Отсутствующий блок означает неверный или неполный вход.
|
||
raise ValueError('Не найден блок ' + keyword) # Не возвращает пустую карту под видом успешной проверки.
|
||
start, depth = match.end(), 1 # Начальная открывающая скобка уже найдена.
|
||
for index in range(start, len(text)): # Вложенные тела обычных секций допускаются.
|
||
depth += (text[index] == '{') - (text[index] == '}') # Подсчитывает только уже очищенный от комментариев текст.
|
||
if not depth: # Нулевой уровень закрывает именно искомую директиву.
|
||
return text[start:index] # Сохраняет внутренние тела секций для дальнейшей проверки.
|
||
raise ValueError('Незакрытый блок ' + keyword) # Усечённый файл не может дать положительный результат.
|
||
|
||
|
||
def parse_cmd(text): # Разбирает числовые регионы и обычные назначения, явно отмечая всё неподдерживаемое.
|
||
text, issues = clean_text(text), [] # Синтаксические ограничения входят в общий отчёт.
|
||
for line in re.findall(r'^\s*(#[^\n]+)', text, re.M): # TI-препроцессор не воспроизводится средствами Python.
|
||
issues.append(issue('incomplete', 'PREPROCESSOR', line.strip() + ': передайте предварительно обработанный CMD TI.')) # include и условные ветки никогда не игнорируются молча.
|
||
if any(len(re.findall(r'\b' + word + r'\s*\{', text)) != 1 for word in ('MEMORY', 'SECTIONS')): # Несколько директив нельзя свести к первому блоку без потери физической карты.
|
||
issues.append(issue('incomplete', 'MULTIPLE_BLOCKS', 'Поддерживается ровно один MEMORY и один SECTIONS; объедините предварительно обработанный CMD.')) # Не выдаёт первый разобранный блок за проверку всего файла.
|
||
memory, section_text = body(text, 'MEMORY'), body(text, 'SECTIONS') # Требует обе основные директивы.
|
||
regions, page = [], 0 # MEMORY без PAGE относится к стандартной странице 0.
|
||
rule = re.compile(r'(' + NAME + r')\s*(?:\([RWIX]+\))?\s*:\s*(?:origin|org)\s*=\s*(' + NUM + r')\s*,?\s*(?:length|len)\s*=\s*(' + NUM + r')\s*[,;]?\s*$', re.I) # Выражения длины и неизвестные атрибуты дают incomplete.
|
||
for raw in memory.splitlines(): # Один числовой MEMORY-регион задаётся одной строкой.
|
||
line = raw.strip() # Отступы и пустые комментарии не влияют на смысл.
|
||
if not line: # Пропускает только заведомо пустые строки.
|
||
continue # Не увеличивает число неподдерживаемых конструкций.
|
||
mark = re.fullmatch(r'PAGE\s+([01])\s*:', line, re.I) # Страница учитывается при разрешении имени региона.
|
||
if mark: # Переключение действует на следующие объявления.
|
||
page = int(mark[1]) # Номер страницы не изменяет физическую адресацию.
|
||
continue # PAGE не является MEMORY-регионом.
|
||
match = rule.fullmatch(line) # Полное совпадение исключает незаметное отбрасывание выражений.
|
||
if match: # Только полностью понятная запись попадает в проверяемую карту.
|
||
regions.append(Region(match[1], page, int(match[2], 0), int(match[3], 0))) # Адрес и длина остаются в словах TI.
|
||
else: # Даже часть корректного региона не подтверждает всю строку.
|
||
issues.append(issue('incomplete', 'MEMORY_SYNTAX', 'Не разобрана строка MEMORY: ' + line)) # Показывает пользователю точную границу анализа.
|
||
heads = list(re.finditer(r'(?m)^\s*(' + NAME + r')\s*:', section_text)) # Обычные выходные секции могут содержать точки в имени.
|
||
if heads and section_text[:heads[0].start()].strip(): # Непонятный текст до первой обычной секции не принадлежит телу уже распознанного назначения.
|
||
issues.append(issue('incomplete', 'SECTION_PREFIX', 'Не разобран префикс SECTIONS перед первым обычным назначением.')) # Не скрывает потерянное назначение .text или неизвестную конструкцию в начале блока.
|
||
sections = [] # Результат связывает LOAD/RUN с конкретными регионами.
|
||
for index, head in enumerate(heads): # Охватывает все обычные назначения, включая многострочные LOAD/RUN.
|
||
text_body = section_text[head.end():heads[index + 1].start() if index + 1 < len(heads) else len(section_text)] # Следующий заголовок завершает текущее назначение.
|
||
target = re.search(r'(?<!>)>\s*(' + NAME + r')', text_body) # Простой > задаёт совпадающие адреса хранения и исполнения.
|
||
load = re.search(r'\bLOAD\s*=\s*(' + NAME + r')', text_body, re.I) # LOAD указывает место хранения секции.
|
||
run = re.search(r'\bRUN\s*=\s*(' + NAME + r')', text_body, re.I) # RUN определяет адрес исполнения копируемого RAM-кода.
|
||
pg = re.search(r'\bPAGE\s*=\s*([01])', text_body, re.I) # Без PAGE однозначность проверяется по всем регионам.
|
||
typ = re.search(r'\bTYPE\s*=\s*(NOLOAD|DSECT|COPY|NOINIT)', text_body, re.I) # Различает программирование и фиктивное представление ROM.
|
||
section = {'name': head[1], 'load': load[1] if load else target[1] if target else None, 'run': run[1] if run else load[1] if load else target[1] if target else None, 'page': int(pg[1]) if pg else None, 'type': typ[1].upper() if typ else '', 'body': text_body.strip()} # Не подставляет предполагаемый регион при неизвестном синтаксисе.
|
||
rest = re.sub(r'\b(?:LOAD|RUN)\s*=\s*' + NAME + r'|(?<!>)>\s*' + NAME + r'|\bPAGE\s*=\s*[01]|\bTYPE\s*=\s*(?:NOLOAD|DSECT|COPY|NOINIT)|\b(?:LOAD_START|LOAD_END|LOAD_SIZE|RUN_START|RUN_END|RUN_SIZE|SIZE)\s*\(\s*' + NAME + r'\s*\)|\*\s*\(\s*[.\w]+\s*\)', '', text_body, flags=re.I) # Поддерживает copy-symbols и выбор входных ramfuncs без полного языка TI.
|
||
if re.sub(r'[\s{},;]', '', rest) or not section['load']: # Неизвестный остаток запрещает полный успех.
|
||
issues.append(issue('incomplete', 'SECTION_SYNTAX', section['name'] + ': назначение разобрано не полностью.')) # Понятные адреса всё ещё можно показать через inspect.
|
||
sections.append(section) # Неполный разбор не скрывает подтверждённые ошибки соседних секций.
|
||
if not regions or not sections: # Пустой частичный результат не является проверкой linker-карты.
|
||
issues.append(issue('incomplete', 'EMPTY_PARSE', 'Не получены MEMORY-регионы или выходные секции.')) # Требует поддерживаемый числовой вход.
|
||
if re.search(r'\b(?:GROUP|UNION|LOAD_PAGE|RUN_PAGE)\b|>>', text): # Раздельные страницы LOAD/RUN и overlay требуют полноценного TI linker.
|
||
issues.append(issue('incomplete', 'ADVANCED_SYNTAX', 'GROUP/UNION, >> и LOAD_PAGE/RUN_PAGE не поддерживаются.')) # Ограничение явно включено в статус 2.
|
||
return regions, sections, issues # Вызывающий код объединяет синтаксические и аппаратные нарушения.
|
||
|
||
|
||
def physical(region, device): # Приводит высокое зеркало L0–L3 F28335 к тем же физическим словам.
|
||
alias = DEVICES[device]['alias'] # F2812 H0 преобразовывать нельзя: это отдельная RAM.
|
||
start = region.origin - alias[0] + alias[2] if alias and alias[0] <= region.origin < region.origin + region.length <= alias[1] else region.origin # Переносит только целиком расположенный в зеркале интервал.
|
||
return start, start + region.length # Во всех расчётах правая граница исключена.
|
||
|
||
|
||
def inside(start, end, ranges): # Проверяет полное вхождение интервала в один известный аппаратный блок.
|
||
return any(left <= start < end <= right for left, right in ranges) # Нулевая длина и выход за конец не принимаются.
|
||
|
||
|
||
def reserves(device, boot): # Описывает стандартные резервы без платоспецифичных EEPROM/Flash API.
|
||
p, begin = DEVICES[device], DEVICES[device]['begin'] # Для 2812 и 28335 адреса безопасности различаются.
|
||
result = [('BOOT_RSVD', 2 if device == '28335' and boot == 'ram' else 0, 0x50), ('CSM_RSVD', begin - 0x76, begin), ('FLASH_BEGIN', begin, begin + 2), ('CSM_PWL', begin + 2, begin + 10), ('OTP', *p['otp']), ('RESET', 0x3FFFC0, 0x400000)] # Первые 80 слов M0 резервируются консервативно; RAM entry28335 занимает только первые 2.
|
||
if device == '28335': # У этой модели имеется заводской код калибровки ADC.
|
||
result.append(('ADC_CAL', 0x380080, 0x380089)) # Приложение не должно перезаписывать девять слов ADC_cal.
|
||
return result # Секции безопасности не получают загружаемых назначений в генераторе.
|
||
|
||
|
||
def validate_cmd(regions, sections, device, boot, active_sections=None): # Проверяет физическую память независимо от логических PAGE.
|
||
issues, p = [], DEVICES[device] # Все аппаратные пределы берутся из выбранного, а не угаданного MCU.
|
||
known = (*p['ram'], p['flash'], p['rom'], p['otp']) + (((0x380080, 0x380089),) if device == '28335' else ()) # Заводской ADC-регион существует только у 28335.
|
||
for index, region in enumerate(regions): # Один регион сравнивается со всеми ранее объявленными.
|
||
start, end = physical(region, device) # Высокое зеркало и обычная RAM теперь имеют общую координату.
|
||
if region.length <= 0 or region.origin + region.length > 0x400000: # Адреса C28x22-битные, а длина должна выделять реальные слова.
|
||
issues.append(issue('error', 'REGION_RANGE', region.name + ': недопустимые адрес или длина.')) # Запрещает выход за адресное пространство.
|
||
if not inside(start, end, known): # Периферия и XINTF нуждаются в дополнительных данных платы.
|
||
issues.append(issue('incomplete', 'UNKNOWN_MEMORY', region.name + ': вне известных блоков RAM/Flash/ROM; внешнюю память и периферию проверьте отдельно.')) # Не объявляет неподтверждённую память конкретной платы безопасной.
|
||
for old in regions[:index]: # Отдельные PAGE не исключают физическое пересечение.
|
||
left, right = physical(old, device) # Учитывает зеркало второго региона.
|
||
if start < right and left < end and inside(max(start, left), min(end, right), (*p['ram'], p['flash'])): # Соседние интервалы разрешены; вложенные ROM-представления не занимают RAM/Flash.
|
||
issues.append(issue('error', 'PHYSICAL_OVERLAP', f'{old.name}/PAGE{old.page} и {region.name}/PAGE{region.page}: общая физическая память 0x{max(start, left):X}..0x{min(end, right):X}.')) # Объясняет источник конфликта, а не только номера страниц.
|
||
for name, left, right in reserves(device, boot): # Общий FLASH/RAM-регион не должен поглощать резерв безопасности.
|
||
special = region.name == name or name == 'FLASH_BEGIN' and region.name == 'BEGIN' and region.origin == p['begin'] and region.length == 2 or name == 'RESET' and region.name in ('RESET', 'VECTORS', 'ROM') # Точные именованные резервы можно описывать без программирования.
|
||
if start < right and left < end and not special: # Проверяет также широкие регионы, пока не занятые секциями.
|
||
issues.append(issue('error', 'RESERVED_REGION', f'{region.name} захватывает {name}: 0x{left:X}..0x{right:X}.')) # Требует выделить защищённый интервал отдельно.
|
||
for section in sections: # Теперь проверяет потенциальные назначения CMD либо только непустые секции переданного MAP.
|
||
if active_sections is not None and section['name'] not in active_sections: # Пустая секция CSM/codestart в RAM COFF не означает фактическую запись Flash.
|
||
continue # При наличии MAP вывод о загрузке делается только по реально размещённым секциям.
|
||
targets = {} # LOAD и RUN остаются разными даже для одинакового адресного пространства.
|
||
for key in ('load', 'run'): # Обе стороны должны однозначно ссылаться на MEMORY.
|
||
matches = [r for r in regions if r.name == section[key] and (section['page'] is None or r.page == section['page'])] # PAGE разрешает имя, не создавая физическую память.
|
||
if len(matches) != 1: # Пропущенное либо неоднозначное назначение не исправляется догадкой.
|
||
issues.append(issue('error', 'SECTION_TARGET', f"{section['name']}: {key.upper()}={section[key]} не определён однозначно; проверьте имя и PAGE.")) # Даёт конкретную сторону LOAD/RUN.
|
||
else: # Однозначный регион доступен для аппаратных проверок.
|
||
targets[key] = matches[0] # Сохраняет назначение без изменения исходной карты.
|
||
if section['type'] not in ('DSECT', 'NOLOAD', 'COPY'): # Эти типы не загружают содержимое в ROM/OTP.
|
||
for target in set(targets.values()): # Обычный совпадающий LOAD/RUN проверяется один раз.
|
||
start, end = physical(target, device) # Зеркала не обходят защиту памяти.
|
||
for name, left, right in reserves(device, boot): # Даже именованный резерв нельзя занять обычной секцией.
|
||
if start < right and left < end and not (name == 'FLASH_BEGIN' and section['name'] == 'codestart' and boot == 'flash'): # Исключение — аппаратный двухсловный branch Flash.
|
||
issues.append(issue('error', 'RESERVED_SECTION', section['name'] + ': загрузка в резерв ' + name)) # CSM/OTP не программируются автоматически.
|
||
if inside(start, end, (p['rom'],)): # Таблицы BootROM существуют, но не являются записываемой памятью.
|
||
issues.append(issue('error', 'ROM_LOAD', section['name'] + ': BootROM допускается только как NOLOAD/DSECT.')) # Не выпускает загружаемую секцию в ROM.
|
||
if boot == 'flash' and section['name'] in ('.text', '.cinit', '.pinit', '.econst', '.switch') and section['type'] not in ('DSECT', 'NOLOAD', 'COPY'): # После холодного Flash reset обычные инициализированные секции должны иметь сохраняемую копию.
|
||
if 'load' not in targets or not inside(*physical(targets['load'], device), (p['flash'],)): # RUN в RAM допустим только при явной стороне LOAD во Flash.
|
||
issues.append(issue('error', 'FLASH_LOAD', section['name'] + ': Flash boot требует LOAD во внутренней Flash; загружаемая только в RAM секция исчезнет после reset.')) # Не принимает JTAG-образ RAM за автономно запускаемую Flash-прошивку.
|
||
if section['name'] == 'codestart' and 'load' in targets: # Проверяет место длинной ветви независимо от имени региона BEGIN.
|
||
target, entry = targets['load'], p['begin'] if boot == 'flash' else p['ram_entry'] # RAM boot2812 использует H0; RAM boot28335 — M0.
|
||
if target.origin != entry or target.length != 2 or section['load'] != section['run'] or section['type'] in ('DSECT', 'NOLOAD', 'COPY'): # Пустое или копируемое представление не запускается аппаратным boot ROM.
|
||
issues.append(issue('error', 'CODESTART', f'codestart должен загружаться в отдельные 2 слова по адресу 0x{entry:X}.')) # Фактическая длина branch дополнительно проверяется MAP.
|
||
if section['name'] in ('ramfuncs', '.TI.ramfunc') and boot == 'flash': # Эти функции исполняются из RAM после копирования из Flash.
|
||
load_ok = 'load' in targets and inside(*physical(targets['load'], device), (p['flash'],)) # Адрес хранения должен принадлежать Flash выбранного MCU.
|
||
run_ok = 'run' in targets and inside(*physical(targets['run'], device), p['ram']) # Адрес исполнения должен принадлежать встроенной RAM.
|
||
symbols_ok = all(re.search(r'\b' + op + r'\s*\(', section['body']) for op in ('LOAD_START', 'RUN_START')) and any(re.search(r'\b' + op + r'\s*\(', section['body']) for op in ('LOAD_SIZE', 'LOAD_END')) # Требует минимальные символы адреса источника, назначения и длины memcpy.
|
||
if not (load_ok and run_ok and symbols_ok): # Обычное размещение ramfuncs во Flash не считается выполнением условия.
|
||
issues.append(issue('error', 'RAMFUNCS_COPY', section['name'] + ': нужны LOAD=Flash, RUN=RAM и LOAD_START/RUN_START и LOAD_SIZE либо LOAD_END.')) # Сам memcpy приложение обязано выполнить до InitFlash.
|
||
if boot == 'flash' and not any(s['name'] == 'codestart' for s in sections): # Прямой запуск main отладчиком не заменяет аппаратную точку входа.
|
||
issues.append(issue('error', 'NO_CODESTART', 'Нет codestart; добавьте TI CodeStartBranch.asm и двухсловный BEGIN.')) # Подсказывает обязательный стартовый исходник.
|
||
if boot == 'flash' and not any(s['name'] in ('ramfuncs', '.TI.ramfunc') for s in sections): # Flash-карта должна описывать место кода настройки Flash в RAM.
|
||
issues.append(issue('error', 'NO_RAMFUNCS', 'Нет ramfuncs/.TI.ramfunc с копированием Flash→RAM.')) # Не обещает безопасную InitFlash только по наличию .text.
|
||
return issues # Неполнота синтаксиса добавляется отдельно результатом parse_cmd.
|
||
|
||
|
||
def generate(device, boot): # Возвращает стартовый CMD COFF без предположений о внешней плате.
|
||
p, regions = DEVICES[device], [] # Берёт профиль ровно указанной модели.
|
||
regions.append(Region('BEGIN', 0, p['begin'] if boot == 'flash' else p['ram_entry'], 2)) # Два слова оставлены только branch codestart.
|
||
if device == '28335': # RAML0–L3 используют только нижний адрес, без одновременного высокоадресного зеркала.
|
||
regions.extend((Region('RAM_CODE', 0, 0x8000, 0x1000 if boot == 'flash' else 0x4000), Region('RAM_DATA', 1, 0x9000 if boot == 'flash' else 0xC000, 0x7000 if boot == 'flash' else 0x4000))) # Код и данные физически разделены, а Flash имеет отдельную RAM для RUN.
|
||
else: # У 2812 H0 выделяется коду при RAM boot и данным при Flash boot.
|
||
regions.extend((Region('RAM_CODE', 0, 0x8000 if boot == 'flash' else 0x3F8002, 0x1000 if boot == 'flash' else 0x1FFE), Region('RAM_DATA', 1, 0x3F8000 if boot == 'flash' else 0x8000, 0x2000))) # H0 не перекрывает L0/L1 и не подменяется XINTF.
|
||
regions.extend((Region('RAMM0', 1, 0x50, 0x3B0), Region('RAMM1', 1, 0x400, 0x400))) # Первые 80 слов M0 консервативно зарезервированы для BootROM; M1 предназначен стеку.
|
||
if boot == 'flash': # Объединённый Flash-регион заканчивается до CSM_RSVD.
|
||
regions.append(Region('FLASH_APP', 0, p['flash'][0], p['begin'] - 0x76 - p['flash'][0])) # Поддерживает доступные секторы без программирования пароля и boot-entry обычными данными.
|
||
regions.extend((Region('RESET', 0, 0x3FFFC0, 2), Region('CSM_RSVD', 0, p['begin'] - 0x76, 0x76), Region('CSM_PWL', 0, p['begin'] + 2, 8))) # CSM показан явно, но не получает загружаемых секций.
|
||
lines = [f'/* Стартовая карта TMS320F{device}, {boot.upper()}, COFF; все адреса и размеры — слова 16 бит. */', '/* Добавьте TI CodeStartBranch.asm и Headers_nonBIOS.cmd; настройте RTS, стек и копирование RAM-функций по README. */', 'MEMORY /* PAGE0/PAGE1 не являются независимой физической RAM. */', '{ /* Начало встроенной памяти выбранного MCU. */'] # Русские пояснения сопровождают каждую генерируемую строку кода.
|
||
for page in (0, 1): # Порядок повторяет обычное оформление примеров TI.
|
||
lines.append(f'PAGE {page}: /* Логическая страница линкера; физические интервалы не дублируются. */') # Не разрешает повторно использовать один адрес на другой странице.
|
||
for region in (r for r in regions if r.page == page): # Выводит каждый регион один раз.
|
||
lines.append(f' {region.name} : origin = 0x{region.origin:06X}, length = 0x{region.length:06X} /* {region.length} слов; аппаратные адреса профиля TI. */') # Значения переносимы между проектами выбранного MCU.
|
||
lines.extend(('} /* Конец MEMORY; внешняя память сюда не добавлена. */', 'SECTIONS /* Назначения стандартных COFF-секций. */', '{ /* Начало выходных секций. */', ' codestart : > BEGIN, PAGE = 0 /* Длинный branch из TI CodeStartBranch.asm в _c_int00. */')) # codestart остаётся отдельным от .text.
|
||
program = 'FLASH_APP' if boot == 'flash' else 'RAM_CODE' # Инициализированный код хранится в выбранном загрузочном носителе.
|
||
for section in ('.text', '.cinit', '.pinit', '.econst', '.switch'): # Имена соответствуют COFF, а не неподдерживаемому EABI.
|
||
lines.append(f' {section} : > {program}, PAGE = 0 /* Код, константы или таблицы инициализации COFF. */') # Не создаёт фиктивной адресуемой области EEPROM.
|
||
if boot == 'flash': # Исторические и современные RAM-функции входят в одну выходную секцию копирования.
|
||
lines.append(' ramfuncs : { *(ramfuncs) *(.TI.ramfunc) } LOAD = FLASH_APP, RUN = RAM_CODE, LOAD_START(_RamfuncsLoadStart), LOAD_END(_RamfuncsLoadEnd), LOAD_SIZE(_RamfuncsLoadSize), RUN_START(_RamfuncsRunStart), PAGE = 0 /* До первого вызова скопируйте LoadSize слов из LoadStart в RunStart. */') # COFF linker-символам с подчёркиванием соответствуют C-имена без него.
|
||
else: # При RAM boot JTAG/boot-таблица загружает код сразу по исполняемому адресу.
|
||
lines.append(' ramfuncs : { *(ramfuncs) *(.TI.ramfunc) } > RAM_CODE, PAGE = 0 /* Отдельное копирование из внутренней Flash не требуется. */') # Не генерирует вымышленные Flash copy-symbols.
|
||
for section in ('.ebss', '.esysmem', '.cio'): # Изменяемые данные обязательно попадают в RAM.
|
||
lines.append(f' {section} : > RAM_DATA, PAGE = 1 /* Переменные, динамическая память или служебный буфер COFF. */') # Фактический объём проверяется линкером и MAP.
|
||
lines.extend((' .stack : > RAMM1, PAGE = 1 /* Размер --stack_size не должен превышать 1024 слова M1. */', ' .reset : > RESET, PAGE = 0, TYPE = DSECT /* Reset-vector находится в BootROM и не программируется. */', '} /* CSM/OTP намеренно не имеют загружаемых секций. */')) # Генерация никогда не создаёт пароль безопасности.
|
||
return '\n'.join(lines) + '\n' # Возвращает UTF-8 текст с конечным переводом строки.
|
||
|
||
|
||
def inspect_map(text, regions, sections, device, boot): # Сверяет поддерживаемый текстовый MAP TI с фактическими назначениями CMD.
|
||
issues, allocations = [], [] # MAP добавляет размеры секций, которых нет в исходном CMD.
|
||
if 'MEMORY CONFIGURATION' not in text or 'SECTION ALLOCATION MAP' not in text: # Не принимает XML/лог или усечённый файл за обычный MAP TI.
|
||
return [issue('incomplete', 'MAP_FORMAT', 'Нужен текстовый MAP TI C2000 с MEMORY CONFIGURATION и SECTION ALLOCATION MAP.')], [] # Частичный формат даёт статус 2.
|
||
memory = text.split('MEMORY CONFIGURATION', 1)[1].split('SECTION ALLOCATION MAP', 1)[0] # Исключает таблицы входных секций из разбора MEMORY.
|
||
map_regions, page = {}, 0 # PAGE участвует в идентификации названия региона.
|
||
for line in memory.splitlines(): # Таблица TI задаёт origin/length/used/unused шестнадцатерично.
|
||
mark = re.fullmatch(r'\s*PAGE\s+([01]):\s*', line) # Переключает только логическую страницу записи.
|
||
if mark: # Новая страница относится к последующим MEMORY-строкам.
|
||
page = int(mark[1]) # Физические интервалы будут проверены независимо от PAGE.
|
||
match = re.match(r'\s*(' + NAME + r')\s+([\da-fA-F]{8})\s+([\da-fA-F]{8})\s+([\da-fA-F]{8})\s+([\da-fA-F]{8})\b', line) # Не путает заголовки с числовыми строками.
|
||
if match: # Только полноценная строка даёт надёжное сравнение с CMD.
|
||
map_regions[match[1], page] = tuple(int(match[i], 16) for i in (2, 3, 4, 5)) # Размеры остаются словами C28x.
|
||
for region in regions: # Каждый понятный исходный регион должен присутствовать в MAP этой же сборки.
|
||
found = map_regions.get((region.name, region.page)) # Не ищет ближайшее совпадение по адресу другой страницы.
|
||
if not found or found[:2] != (region.origin, region.length): # Выявляет старый MAP и линковку с другим CMD.
|
||
issues.append(issue('error', 'MAP_MEMORY', region.name + ': origin/length/PAGE MAP не совпадают с CMD.')) # Объясняет необходимость передать карту текущей линковки.
|
||
table = text.split('SECTION ALLOCATION MAP', 1)[1].split('GLOBAL SYMBOLS', 1)[0] # Глобальные символы не являются отдельными выделениями секций.
|
||
table = re.sub(r'(?m)^(' + NAME + r')[ \t]*\n[ \t]*\*?[ \t]+([01])\s+([\da-fA-F]{8})', r'\1 \2 \3', table) # TI переносит длинное имя выходной секции на предыдущую строку.
|
||
heads = list(re.finditer(r'(?m)^(' + NAME + r')\s+\*?\s*([01])\s+([\da-fA-F]{8})\s+([\da-fA-F]{8})([^\n]*)', table)) # Строки input objects имеют отступ и не дублируются.
|
||
for index, match in enumerate(heads): # Фактические непустые секции проверяются обеими сторонами LOAD/RUN.
|
||
name, page, origin, length = match[1], int(match[2]), int(match[3], 16), int(match[4], 16) # Все числовые колонки TI представлены без префикса 0x.
|
||
rule = next((s for s in sections if s['name'] == name), None) # Связывает результат линковки с исходным назначением.
|
||
if not rule: # MAP может включать дополнительные headers.cmd, не переданные анализатору.
|
||
issues.append(issue('incomplete', 'MAP_SECTION', name + ': секция отсутствует в переданном CMD; полный объединённый CMD не проверен.')) # Отсутствие знания явно снижает полноту результата.
|
||
continue # Не угадывает назначение периферийных секций.
|
||
if not length or rule['type'] in ('DSECT', 'COPY'): # Пустые и фиктивные секции не занимают физические слова приложения.
|
||
continue # Не создаёт ложного пересечения ROM-reset с загрузкой.
|
||
attrs = match[5] + table[match.end():heads[index + 1].start() if index + 1 < len(heads) else len(table)] # RUN ADDR может быть напечатан на следующей строке атрибутов.
|
||
run = re.search(r'RUN\s+ADDR\s*=\s*([\da-fA-F]{8})', attrs) # Адрес исполнения нельзя подменять Flash-адресом хранения.
|
||
addresses = [('load', origin), ('run', int(run[1], 16))] if run else [('load', origin)] # Копируемая секция реально занимает два интервала.
|
||
if rule['load'] != rule['run'] and not run: # Неподдерживаемое оформление MAP нельзя объявлять полной проверкой RUN.
|
||
issues.append(issue('incomplete', 'MAP_RUN', name + ': не найден RUN ADDR для раздельного LOAD/RUN.')) # Показывает предел конкретного MAP-парсера.
|
||
for kind, address in addresses: # Каждый интервал должен помещаться в указанном MEMORY-регионе.
|
||
target = next((r for r in regions if r.name == rule[kind] and (rule['page'] is None or r.page == rule['page'])), None) # Повторяет однозначное разрешение назначения TI.
|
||
if not target or not target.origin <= address < address + length <= target.origin + target.length: # Проверяет конец всей секции, а не только её начало.
|
||
issues.append(issue('error', 'MAP_ALLOCATION', f'{name}/{kind}: 0x{address:X}+0x{length:X} вне назначенного MEMORY.')) # Конкретные адрес и размер помогают исправить карту.
|
||
current = Region(name + '/' + kind, page, address, length) # Фактическая запись пригодна для общей проверки физического пересечения.
|
||
start, end = physical(current, device) # Высокое зеркало F28335 нормализуется как в CMD.
|
||
for old in allocations: # PAGE не является исключением из физической проверки.
|
||
left, right = physical(old, device) # Оба сравниваемых интервала имеют одинаковую систему адресов.
|
||
if start < right and left < end: # Конец, равный началу следующей секции, разрешён.
|
||
issues.append(issue('error', 'MAP_OVERLAP', current.name + ' пересекается с ' + old.name)) # Выявляет также конфликт Flash LOAD с другой секцией.
|
||
allocations.append(current) # Следующая секция будет сравниваться с этим размещением.
|
||
if name == 'codestart' and (origin != (DEVICES[device]['begin'] if boot == 'flash' else DEVICES[device]['ram_entry']) or length != 2): # Реальная ветвь должна занимать два слова в аппаратной точке входа.
|
||
issues.append(issue('error', 'MAP_CODESTART', 'Размер/адрес codestart не соответствует выбранному boot mode.')) # Подтверждает то, что невозможно узнать из одного назначения >BEGIN.
|
||
if name in ('ramfuncs', '.TI.ramfunc') and boot == 'flash': # Проверяет наличие именно созданных copy-symbols текущей секции.
|
||
expected = {'LOAD_START': origin, 'LOAD_END': origin + length, 'LOAD_SIZE': length, 'RUN_START': int(run[1], 16) if run else None} # Символы должны соответствовать фактическим LOAD/RUN, а не старой сборке.
|
||
for op, value in expected.items(): # LOAD_END необязателен в пользовательском CMD; генератор всегда его создаёт.
|
||
symbol = re.search(r'\b' + op + r'\s*\(\s*(' + NAME + r')\s*\)', rule['body']) # Имя может отличаться от стандартного Ramfuncs.
|
||
if symbol: # Проверяет только явно заказанные символы, сохраняя минимальный контракт LOAD_SIZE.
|
||
values = re.findall(r'(?m)^\s*(?:(?:[01]|[A-Za-z_]\w*)\s+)?([\da-fA-F]{8})\s+' + re.escape(symbol[1]) + r'\b', text) # TI печатает символы также в нескольких отсортированных таблицах.
|
||
if not values or value is None or any(int(item, 16) != value for item in values): # Наличие слова имени само по себе не подтверждает правильный адрес или размер.
|
||
issues.append(issue('error', 'MAP_COPY_SYMBOL', symbol[1] + ': значение MAP не соответствует выделенной секции.')) # Не принимает ложные символы копирования.
|
||
if boot == 'flash' and not any(r.name == 'codestart/load' for r in allocations): # Flash boot нуждается в реально загружаемой ветви; одно пустое назначение CMD недостаточно.
|
||
issues.append(issue('error', 'MAP_NO_CODESTART', 'MAP не содержит непустой codestart для аппаратного Flash boot.')) # Не подтверждает Flash-образ, запускаемый только прямым переходом отладчика.
|
||
if not heads: # Пустой результат не подтверждает реальную укладку приложения.
|
||
issues.append(issue('incomplete', 'MAP_EMPTY', 'Не распознаны выходные секции текстового MAP TI.')) # Просит поддерживаемый MAP вместо ложного успеха.
|
||
return issues, allocations # Возвращает фактические слова также для JSON/inspect.
|
||
|
||
|
||
def main(argv=None): # Выполняет только явно выбранное офлайн-действие.
|
||
parser = argparse.ArgumentParser(description=__doc__) # Русская справка объясняет применение отдельной утилиты.
|
||
sub = parser.add_subparsers(dest='command', required=True) # По умолчанию пользовательские файлы не создаются.
|
||
sub.add_parser('profiles', aliases=['list'], help='Список MCU, boot modes и ABI.') # Не требует SDK или проекта.
|
||
exp = sub.add_parser('explain', help='Объяснить память и резервы MCU.') # Поясняет единицы и аппаратные границы.
|
||
exp.add_argument('--device', choices=DEVICES, required=True, help='MCU:28335 или 2812.') # Не угадывает похожую модель устройства.
|
||
gen = sub.add_parser('generate', help='Создать стартовый CMD COFF.') # Единственное действие записи входных файлов.
|
||
gen.add_argument('--output', type=Path, required=True, help='Создаваемый .cmd.') # Местоположение задаёт пользователь.
|
||
gen.add_argument('--force', action='store_true', help='Явно разрешить замену существующего файла.') # Без ключа файл защищён от перезаписи.
|
||
readers = [sub.add_parser(name, help='Показать и проверить понятную часть CMD/MAP.') for name in ('inspect', 'validate')] # Оба режима читают существующие карты без редактирования.
|
||
for command in [gen, *readers]: # Аппаратные параметры нужны всем действиям с картой.
|
||
command.add_argument('--device', choices=DEVICES, required=True, help='Ожидаемый MCU.') # Выбор устройства определяет карту, CSM и точку boot.
|
||
command.add_argument('--boot', choices=('ram', 'flash'), required=True, help='RAM boot/JTAG либо внутренняя Flash.') # EEPROM boot-таблица относится к ram.
|
||
for command in readers: # Только режимы проверки принимают исходные CMD/MAP.
|
||
command.add_argument('cmd', type=Path, help='Исходный или предварительно обработанный CMD TI.') # include не читаются и не вычисляются Python автоматически.
|
||
command.add_argument('--map', type=Path, help='Текстовый MAP той же линковки.') # Даёт фактические размеры и LOAD/RUN после TI linker.
|
||
command.add_argument('--json', action='store_true', help='Структурированный JSON UTF-8.') # Отчёт пригоден для непрерывной сборки.
|
||
args = parser.parse_args(argv) # Ошибочный CLI получает стандартный ненулевой статус argparse.
|
||
if args.command in ('profiles', 'list'): # Каталог не связан с текущим проектом.
|
||
print('TMS320F28335 иTMS320F2812: ram/flash, COFF. EABI не поддерживается.') # Не обещает непроверенный ABI.
|
||
return 0 # Простой вывод списка не является аппаратной проверкой.
|
||
if args.command == 'explain': # Сводка связывает числа с обязательными действиями при старте.
|
||
p = DEVICES[args.device] # Выбирает аппаратные диапазоны для понятной человеку таблицы hex-адресов.
|
||
print('TMS320F' + args.device + ' / COFF / адреса и размеры в 16-битных словах') # Единицы C28x явно отличаются от обычных октетов файла.
|
||
for title, start, end in [('RAM', a, b) for a, b in p['ram']] + [('Flash', *p['flash']), ('Flash BEGIN', p['begin'], p['begin'] + 2), ('CSM reserve', p['begin'] - 0x76, p['begin']), ('CSM password', p['begin'] + 2, p['begin'] + 10), ('OTP', *p['otp'])]: # Выводит только общие встроенные блоки выбранной модели.
|
||
print(f'{title:14} 0x{start:06X}..0x{end - 1:06X} {end - start:6} слов') # Включённый конец удобен для сопоставления с таблицей datasheet.
|
||
print('TI flags: --abi=coffabi -v28 --float_support=softlib; F2812 не имеет FPU32.') # Стартовый профиль переносится между обеими моделями без аппаратной FPU.
|
||
print('Внешняя EEPROM хранит boot-таблицу и не увеличивает RAM; объём RAM проверяйте по MAP.') # Большая M95M02 не устраняет переполнение .text/.ebss в RAM.
|
||
print('Адреса и длины — слова 16 бит. PAGE0/PAGE1 физически общие. BEGIN —2 слова. CSM/OTP/BootROM не программируются. Flash ramfuncs копируются до InitFlash. См.README.') # Объясняет назначение резервов и LOAD/RUN.
|
||
return 0 # Пользовательская карта в этой команде не проверяется.
|
||
try: # Обычные ошибки пути и синтаксиса не скрываются traceback.
|
||
if args.command == 'generate': # Выполняет только явно запрошенную запись.
|
||
text = generate(args.device, args.boot) # Получает готовый UTF-8 шаблон COFF.
|
||
regions, sections, issues = parse_cmd(text) # Проверяет встроенную карту тем же parser, что пользовательскую.
|
||
issues += validate_cmd(regions, sections, args.device, args.boot) # Ошибка генератора не должна попасть в проект.
|
||
if issues: # Собственный шаблон обязан поддерживаться целиком.
|
||
raise ValueError('; '.join(i['message'] for i in issues)) # Не сохраняет сомнительный или частично проверенный CMD.
|
||
with args.output.open('w' if args.force else 'x', encoding='utf-8', newline='\n') as stream: # Режим x атомарно защищает существующий файл от случайной замены.
|
||
stream.write(text) # Каждая строка CMD сопровождается русским комментарием.
|
||
print('Создан ' + str(args.output) + '; добавьте стартовый код и проверьте MAP/README.') # Шаблон не считается готовой прошивкой платы.
|
||
return 0 # Генератор не запускает программатор или сторонний процесс.
|
||
regions, sections, issues = parse_cmd(args.cmd.read_text(encoding='utf-8-sig')) # Поддерживает пользовательский UTF-8 с BOM без изменения файла.
|
||
allocations = [] # Без MAP реальный размер секций неизвестен.
|
||
if args.map: # Дополнительная проверка выполняется только по явно переданной карте.
|
||
extra, allocations = inspect_map(args.map.read_text(encoding='utf-8-sig'), regions, sections, args.device, args.boot) # Не читает COFF и бинарные прошивки.
|
||
issues += extra # Ограничения MAP также участвуют в итоговом статусе.
|
||
active = {r.name.rsplit('/', 1)[0] for r in allocations} if args.map else None # Без MAP все назначения потенциальны; с MAP пустые CSM-секции не выдаются за запись Flash.
|
||
issues += validate_cmd(regions, sections, args.device, args.boot, active) # Физические MEMORY-пересечения проверяются всегда; фактическая загрузка секций уточняется MAP.
|
||
status = 1 if any(i['level'] == 'error' for i in issues) else 2 if issues else 0 # Код 1 — найденное нарушение, код 2 — неполная проверка.
|
||
report = {'status': status, 'scope': 'Подмножество CMD; без MAP размеры секций неизвестны. TI linker и стартовый код проверяются отдельно.', 'device': args.device, 'boot': args.boot, 'regions': [r.__dict__ for r in regions], 'sections': sections, 'allocations': [r.__dict__ for r in allocations], 'issues': issues} # Машинный результат не теряет пределы применимости анализа.
|
||
if args.json: # JSON не смешивается с обычными строками stdout.
|
||
print(json.dumps(report, ensure_ascii=False, indent=2)) # Русские пояснения остаются читаемыми UTF-8.
|
||
else: # Человеческий режим показывает карту и все ограничения.
|
||
for region in regions: # Страница выводится рядом с физическим адресом для объяснения конфликта.
|
||
print(f'{region.name:14} PAGE{region.page} 0x{region.origin:06X}..0x{region.origin + region.length - 1:06X} ({region.length} слов)') # Правая граница включённая только при выводе человеку.
|
||
for item in issues: # Неподдерживаемая конструкция не скрывается за успешным частичным разбором.
|
||
print(f"{item['level'].upper()} {item['code']}: {item['message']}") # Стабильный код помогает автоматизировать диагностику.
|
||
print('Проверенная часть: нарушений не найдено; размеры требуют MAP/TI linker.' if not issues else 'Есть ошибки/непроверенные конструкции; вся карта не подтверждена.') # Не приравнивает статический успех к полной аппаратной проверке.
|
||
return status # Возвращает результат без изменения CMD/MAP.
|
||
except (OSError, ValueError) as error: # Отсутствующий путь, существующий файл и неверный формат имеют понятный отказ.
|
||
print('ОШИБКА: ' + str(error), file=sys.stderr) # stderr не портит структурированный успешный отчёт.
|
||
return 1 # Неудача чтения/генерации не может считаться проверкой.
|
||
|
||
if __name__ == '__main__': # Импорт в unittest не выполняет CLI и не создаёт файлы.
|
||
raise SystemExit(main()) # Передаёт точный статус процессу сборки.
|