Files
templates/tools/c2000-linker/c2000_linker.py
T
Andrey 6ecf057a86 feat(c2000-linker): Добавить помощник настройки линкера C2000
Добавить генерацию карт COFF для TMS320F28335 и TMS320F2812, проверку CMD/MAP и русскую инструкцию.

Проверки: 14 тестов, включая четыре реальные линковки TI для RAM и Flash.
2026-10-19 16:33:40 +03:00

318 lines
54 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.
"""Создаёт и проверяет стартовые карты 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()) # Передаёт точный статус процессу сборки.