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