From 6ecf057a86481323bc0d4b9313e1d5774da84802 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 19 Oct 2026 16:33:40 +0300 Subject: [PATCH] =?UTF-8?q?feat(c2000-linker):=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BE=D0=BC=D0=BE=D1=89=D0=BD?= =?UTF-8?q?=D0=B8=D0=BA=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BB=D0=B8=D0=BD=D0=BA=D0=B5=D1=80=D0=B0=20C2000?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Добавить генерацию карт COFF для TMS320F28335 и TMS320F2812, проверку CMD/MAP и русскую инструкцию. Проверки: 14 тестов, включая четыре реальные линковки TI для RAM и Flash. --- tools/c2000-linker/.gitignore | 4 + tools/c2000-linker/README.md | 83 +++++++ tools/c2000-linker/TEST_RESULTS.md | 22 ++ tools/c2000-linker/c2000_linker.py | 317 ++++++++++++++++++++++++++ tools/c2000-linker/tests/run_tests.py | 112 +++++++++ 5 files changed, 538 insertions(+) create mode 100644 tools/c2000-linker/.gitignore create mode 100644 tools/c2000-linker/README.md create mode 100644 tools/c2000-linker/TEST_RESULTS.md create mode 100644 tools/c2000-linker/c2000_linker.py create mode 100644 tools/c2000-linker/tests/run_tests.py diff --git a/tools/c2000-linker/.gitignore b/tools/c2000-linker/.gitignore new file mode 100644 index 0000000..919cfec --- /dev/null +++ b/tools/c2000-linker/.gitignore @@ -0,0 +1,4 @@ +# Кэш Python создаётся локальными проверками и не входит в переносимый исходный комплект. +__pycache__/ +# Скомпилированные модули не являются поставляемыми исходниками helper. +*.pyc diff --git a/tools/c2000-linker/README.md b/tools/c2000-linker/README.md new file mode 100644 index 0000000..55569bd --- /dev/null +++ b/tools/c2000-linker/README.md @@ -0,0 +1,83 @@ +# Помощник linker для TMS320F28335 и TMS320F2812 + +Переносимая утилита Python 3.10+ из стандартной библиотеки. Она создаёт стартовые карты **COFF** для RAM и внутренней Flash, объясняет память и проверяет поддерживаемую часть текстовых CMD/MAP. Работает офлайн без SDK, платы и программатора. EABI сейчас не поддерживается. + +Все адреса, длины MEMORY, размеры секций и copy-symbols выражены в **16-битных словах C28x**. Например, `0x1000` слов занимает 8192 физических октета. PAGE 0 и PAGE 1 — логические страницы линкера; одинаковые адреса RAM/Flash на них не означают независимую память. Высокое зеркало L0–L3 F28335 также не даёт дополнительную RAM. H0 F2812 — самостоятельный блок. + +## Быстрый запуск + +Команды приведены относительно корня общего `templates`: + +```powershell +python tools/c2000-linker/c2000_linker.py profiles # Показывает поддерживаемые устройства, boot modes и ABI. +python tools/c2000-linker/c2000_linker.py explain --device 2812 # Объясняет адреса RAM, Flash, BEGIN, CSM и OTP в hex. +python tools/c2000-linker/c2000_linker.py generate --device 28335 --boot flash --output board_flash.cmd # Создаёт стартовый COFF CMD для внутренней Flash. +python tools/c2000-linker/c2000_linker.py generate --device 2812 --boot ram --output board_ram.cmd # Создаёт отдельную RAM-карту F2812 с входом в H0. +python tools/c2000-linker/c2000_linker.py inspect board_flash.cmd --device 28335 --boot flash # Показывает регионы и потенциальные назначения до линковки. +python tools/c2000-linker/c2000_linker.py validate board_flash.cmd --device 28335 --boot flash --map build/board.map --json # Сверяет фактические адреса и размеры MAP той же сборки. +``` + +`generate` атомарно отказывается заменять существующий файл. Для осознанной замены используйте `--force`. Утилита не создаёт родительские каталоги молча, не переписывает исходный CMD при проверке и не меняет проект CCS. + +## Подключение карты к проекту + +1. Выберите ровно нужный MCU, `ram`/`flash` и COFF. Сгенерируйте новый CMD и добавьте его в проект вместо прежней карты памяти. Периферийный `DSP2833x_Headers_nonBIOS.cmd` либо соответствующий F281x headers CMD подключается отдельно из TI SDK: генератор не копирует регистры периферии. +2. Добавьте официальный `CodeStartBranch.asm` выбранного семейства, чтобы `codestart` содержал длинную ветвь к C runtime. Минимальная ASM-ветвь тестов проверяет только синтаксис/адреса линкера и не заменяет штатный стартовый код TI. +3. Согласуйте ABI, RTS-библиотеку и параметры всех объектов. Общий проверенный минимум — `--abi=coffabi -v28 --float_support=softlib`. **F2812 не имеет FPU32**. Линковке обычно нужны `--rom_model`, `--stack_size=0x100`, `--heap_size=0x40` и вывод `--map_file=...`. Размеры стека/heap подбираются для приложения; приведённые значения служат примером. Не превышайте 1024 слова M1 выделенного стеку шаблона. +4. Для Flash скопируйте `ramfuncs` из LOAD во Flash в RUN в RAM **до первого вызова** функции из этой секции, в частности `InitFlash`. Проверьте порядок внутри своего `InitSysCtrl`. Генератор объединяет входные секции `ramfuncs` и `.TI.ramfunc` в один выходной блок и создаёт COFF-символы `_RamfuncsLoadStart`, `_RamfuncsLoadEnd`, `_RamfuncsLoadSize`, `_RamfuncsRunStart`. +5. Выполните настоящую TI линковку и проверьте её MAP. После добавления реальных модулей места может не хватить даже при корректной начальной карте; утилита не выбирает автоматически split-секции, trampolines или overlay. + +Пример использования linker-символов из C COFF: + +```c +#include /* Объявляет memcpy; на C28x один char соответствует адресуемому 16-битному слову. */ +extern unsigned int RamfuncsLoadStart, RamfuncsLoadSize, RamfuncsRunStart; /* C-имена соответствуют linker-именам с ведущим подчёркиванием COFF. */ +memcpy(&RamfuncsRunStart, &RamfuncsLoadStart, (size_t)&RamfuncsLoadSize); /* Адрес символа LoadSize кодирует число копируемых слов; значение по этому адресу не читается. */ +``` + +Эту операцию помещают в стартовую функцию, не в глобальную область C. Допустим и исторический контракт `LOAD_END`: размер вычисляется по разности адресов `LoadEnd` и `LoadStart`. Проверка принимает `LOAD_SIZE` **или** `LOAD_END` вместе с обоими начальными адресами. + +Flash API, обработчики во время erase/program, частоты/тайминги Flash и внешняя SRAM зависят от приложения и платы. Генератор их не добавляет и не запускает erase/program. Области CSM/OTP не получают загружаемых секций и паролей. + +## Что именно проверяется + +Поддерживаются буквальные десятичные/hex `origin`/`length` (`org`/`len`), один регион на строку, PAGE 0/1 и обычные SECTIONS: `> REGION`, `LOAD = REGION`, `RUN = REGION`, `PAGE`, `TYPE`, `LOAD_START/END/SIZE`, `RUN_START/END/SIZE`, выбор входных `*(ramfuncs)`/`*(.TI.ramfunc)`. + +Находятся физические пересечения RAM/Flash между PAGE и зеркалами F28335, выход за известную встроенную память, поглощение BEGIN/CSM/OTP/reset/ADC_cal общим регионом, загрузка обычных секций в резервы/BootROM, ошибочный Flash `codestart`, потеря постоянной Flash LOAD-копии обычных инициализированных секций и отсутствие контракта копирования RAM-функций. Первые `0x50` слов M0 консервативно оставлены загрузчику; при RAM boot F28335 первые два слова выделены BEGIN. + +С `--map` проверяются текстовые таблицы TI `MEMORY CONFIGURATION`/`SECTION ALLOCATION MAP`, соответствие MEMORY текущему CMD, непустые фактические LOAD/RUN, размещение секций внутри регионов, пересечения и значения copy-symbols. Поддержан перенос имени секции и отметка `*` в MAP TI CGT 22.6. Загружаемый Flash `codestart` должен реально занимать два слова. Без MAP анализируются **потенциальные назначения**, а фактические размеры неизвестны. + +Коды завершения: + +| Код | Значение | +|---|---| +| `0` | В поддерживаемой части не найдено нарушений. Это не проверка стартового C/ASM-кода или аппаратуры. | +| `1` | Найдено нарушение карты либо не удалось прочитать/создать файл. | +| `2` | Проверка неполная: неподдерживаемый синтаксис/память/MAP; также обычная ошибка CLI argparse. | + +При наличии и ошибок, и неполных частей возвращается `1`; все причины остаются в отчёте. JSON содержит `scope`, `issues`, `regions`, `sections` и фактические `allocations`. + +`#include`, `#if`, макросы, выражения в MEMORY, `GROUP`/`UNION`, отдельные `LOAD_PAGE`/`RUN_PAGE`, split `>>`, внешние регионы и чужие MAP-форматы явно дают неполную проверку. TI linker с препроцессором остаётся окончательным инструментом. Передавайте предварительно обработанный числовой CMD, если нужно убрать влияние include/условных ветвей; помощник не исполняет их сам. + +Исходный CMD проекта может перечислять внутреннюю Flash, CSM и `codestart`, хотя RAM COFF/SCI8 загружает только RAM, а перечисленные выходные секции пусты. Поэтому без MAP предупреждение о потенциальной загрузке в CSM не доказывает запись этих слов. С MAP пустые CSM/codestart-секции не считаются фактической записью. RAM COFF может запускаться JTAG или boot-таблицей по её entry point без аппаратного Boot-to-SARAM `codestart`; отсутствие такой секции не считается ошибкой RAM-профиля. Сгенерированный RAM-шаблон всё же предоставляет стандартный BEGIN, подтверждённый тестом TI. + +Внешняя SPI EEPROM хранит SCI8 boot-таблицу и **не расширяет RAM процессора**. Переход с AT25256B на M95M02 увеличивает ёмкость носителя, но не устраняет переполнение RAM при укладке `.text`, `.cinit`, `.ebss`. Она не является CPU-addressable MEMORY-регионом. + +## Проверки + +```powershell +python tools/c2000-linker/tests/run_tests.py -v # Выполняет переносимые stdlib-регрессии без установленного SDK. +python tools/c2000-linker/tests/run_tests.py --ti C:/ti/ccs1230/ccs/tools/compiler/ti-cgt-c2000_22.6.0.LTS -v # Дополнительно компилирует и линкует4 настоящих COFF-профиля, без платы. +``` + +Тесты проверяют обе модели и оба режима, пересечения разных PAGE и зеркала, резерв CSM/OTP, copy-symbols и альтернативный LOAD_END, отказ перезаписи и явную неполноту неподдерживаемого входа. TI smoke создаёт `.text`, `.econst`, `.ebss`, обе входные RAM-секции и `codestart`, затем анализирует **настоящий MAP TI**. Временные C/ASM/OBJ/OUT/MAP удаляются вместе с собственным временным каталогом. Сохранённый результат выполненной проверки — `TEST_RESULTS.md`. + +## Первичные источники и происхождение адресов + +- [TI TMS320F2833x datasheet, SPRS439Q](https://www.ti.com/lit/ds/symlink/tms320f28335.pdf): карта F28335, зеркальные L0–L3, Flash, boot entry, CSM и заводской ADC_cal. +- [TI TMS320F281x datasheet, SPRS174V](https://www.ti.com/lit/ds/symlink/tms320f2812.pdf): раздел 9.3, таблица 9-15 и boot modes; отдельный H0, Flash, OTP, BEGIN и CSM F2812. +- [TI C28x Assembly Language Tools, linker](https://downloads.ti.com/docs/esd/SPRU513P/Content/SPRU513P_HTML/linker-description.html): MEMORY/PAGE, SECTIONS, LOAD/RUN и linker-generated copy-symbols. +- [TI Linker Command File Primer](https://software-dl.ti.com/ccs/esd/documents/sdto_cgt_Linker-Command-File-Primer.html): физическая память и логические страницы C28x. +- [TI Running an Application from Internal Flash, SPRA958L](https://www.ti.com/lit/an/spra958l/spra958l.pdf): boot-to-Flash, перенос RAM-функций и стартовая настройка Flash. + +Числа дополнительно сопоставлены с локальными официальными TI примерами `C2000Ware_5_03_00_00/device_support/f2833x/common/cmd/F28335.cmd`, `28335_RAM_lnk.cmd` и `controlSUITE/libs/math/IQmath/v160/examples/cmd/2812_IQmath_lnk.cmd`. Генератор содержит собственную компактную карту на основе документированных аппаратных фактов; не копирует целиком распространяемые TI linker-файлы и не требует этих абсолютных путей у пользователя. diff --git a/tools/c2000-linker/TEST_RESULTS.md b/tools/c2000-linker/TEST_RESULTS.md new file mode 100644 index 0000000..c6f53b5 --- /dev/null +++ b/tools/c2000-linker/TEST_RESULTS.md @@ -0,0 +1,22 @@ +# Выполненная проверка + +Дата: 19.10.2026. Python 3.13; TI C2000 CGT 22.6.0.LTS для Windows. + +Команда: + +```powershell +python tests/run_tests.py --ti C:/ti/ccs1230/ccs/tools/compiler/ti-cgt-c2000_22.6.0.LTS -v # Выполняет переносимые регрессии и четыре настоящие COFF-линковки. +``` + +Результат: **14 тестов PASS**, включая четыре сочетания: + +| MCU | Режим | TI compiler/linker | CMD/MAP helper | +|---|---|---|---| +| TMS320F28335 | RAM | PASS | PASS | +| TMS320F28335 | Flash | PASS | PASS | +| TMS320F2812 | RAM | PASS | PASS | +| TMS320F2812 | Flash | PASS | PASS | + +В каждой линковке использованы `--abi=coffabi -v28 --float_support=softlib`; fixture содержит `.text`, `.econst`, `.ebss`, `ramfuncs`, `.TI.ramfunc` и двухсловный `codestart`. Проверены фактические LOAD/RUN и значения Flash copy-symbols в MAP TI. + +Регрессии Python дополнительно подтвердили пересечение RAM между PAGE, конфликт высокого зеркала F28335, CSM/OTP, альтернативный LOAD_END, защиту существующего файла и неполную проверку неподдерживаемых конструкций. Прошивка в MCU не записывалась; smoke не проверяет стартовый код и периферию конкретной платы. diff --git a/tools/c2000-linker/c2000_linker.py b/tools/c2000-linker/c2000_linker.py new file mode 100644 index 0000000..30ad0e3 --- /dev/null +++ b/tools/c2000-linker/c2000_linker.py @@ -0,0 +1,317 @@ +"""Создаёт и проверяет стартовые карты 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()) # Передаёт точный статус процессу сборки. diff --git a/tools/c2000-linker/tests/run_tests.py b/tools/c2000-linker/tests/run_tests.py new file mode 100644 index 0000000..3cc8f35 --- /dev/null +++ b/tools/c2000-linker/tests/run_tests.py @@ -0,0 +1,112 @@ +"""Регрессии карт памяти и необязательная реальная COFF-линковка четырёх профилей TI.""" # Проверяет полезные свойства карты, не повторяя реализацию parser. +import argparse # Принимает необязательный путь к установленному TI CGT для smoke. +import contextlib # Перехватывает stdout CLI при проверке отказа перезаписи. +import importlib.util # Импортирует helper относительно каталога теста без установки пакета. +import re # Разбирает контекст диагностической строки MAP при отказе smoke, не меняя рабочий parser. +import io # Сохраняет русскую диагностику CLI в памяти теста. +from pathlib import Path # Временные CMD/MAP не пишутся в пользовательский проект. +import subprocess # Вызывает настоящий TI compiler/linker только по явному параметру --ti. +import sys # Даёт возможность запускать тесты обычным Python3. +import tempfile # Удаляет только собственные временные сборки после тестов. +import unittest # Стандартная библиотека не требует pytest или скачивания зависимостей. +ROOT = Path(__file__).resolve().parents[1] # Находит установленную утилиту независимо от рабочего каталога. +SPEC = importlib.util.spec_from_file_location('c2000_linker', ROOT / 'c2000_linker.py') # Загружает именно проверяемый файл helper. +HELPER = importlib.util.module_from_spec(SPEC) # Создаёт модуль без запуска CLI. +sys.modules[SPEC.name] = HELPER # dataclass находит собственный модуль при аннотировании Region. +SPEC.loader.exec_module(HELPER) # Исполняет только определения функций и числовых профилей. +TI_ROOT = None # Путь compiler задаётся пользователем тестов, не ищется в глобальной системе молча. + + +def checked(text, device='28335', boot='flash'): # Проверяет сгенерированный или намеренно повреждённый CMD реальными функциями helper. + regions, sections, issues = HELPER.parse_cmd(text) # Использует рабочий parser, а не его тестовую копию. + return regions, sections, issues + HELPER.validate_cmd(regions, sections, device, boot) # Сохраняет и неполный синтаксис, и аппаратные нарушения. + + +class MapsTest(unittest.TestCase): # Проверяет границы ответственности и основные причины реальных ошибок linker. + def test_four_profiles(self): # Обе модели и оба boot mode должны генерироваться без неполной проверки. + for device in ('28335', '2812'): # F2812 не наследует адреса Flash или зеркала F28335. + for boot in ('ram', 'flash'): # RAM boot и внутренняя Flash имеют разные BEGIN. + with self.subTest(device=device, boot=boot): # Показывает конкретный аппаратный профиль при регрессии. + regions, sections, issues = checked(HELPER.generate(device, boot), device, boot) # Проверяет готовый текст, включая каждый русский комментарий CMD. + self.assertEqual(issues, []) # Собственные профили обязаны поддерживаться полностью. + self.assertEqual(next(r for r in regions if r.name == 'BEGIN').origin, HELPER.DEVICES[device]['begin' if boot == 'flash' else 'ram_entry']) # BEGIN соответствует первичным адресам TI. + def test_overlap_across_pages(self): # PAGE0/PAGE1 не дают две независимые копии L0. + text = HELPER.generate('28335', 'flash').replace('RAM_DATA : origin = 0x009000', 'RAM_DATA : origin = 0x008000') # Намеренно размещает данные поверх RAM-функций на другой странице. + self.assertIn('PHYSICAL_OVERLAP', [i['code'] for i in checked(text)[2]]) # Ошибка обнаруживается до запуска TI. + def test_high_alias(self): # Высокое зеркало L0–L3 также не добавляет физической RAM. + text = HELPER.generate('28335', 'ram').replace('RAM_DATA : origin = 0x00C000', 'RAM_DATA : origin = 0x3F8000') # Данные используют высокое зеркало того же L0–L3, где лежит код. + self.assertIn('PHYSICAL_OVERLAP', [i['code'] for i in checked(text, boot='ram')[2]]) # Проверка сравнивает физические адреса, а не строки чисел. + def test_flash_reserved(self): # Широкий FLASH_APP не может поглотить CSM_RSVD, BEGIN и пароль. + text = HELPER.generate('2812', 'flash').replace('length = 0x01FF80', 'length = 0x020000') # Расширяет код до физического конца Flash поверх служебных слов. + self.assertIn('RESERVED_REGION', [i['code'] for i in checked(text, '2812')[2]]) # Адреса 2812 сверяются по собственному профилю. + def test_otp_write(self): # Отдельное имя OTP не разрешает программировать его обычной секцией. + text = HELPER.generate('28335', 'flash').replace('PAGE 0:', 'PAGE 0:\n OTP : origin = 0x380400, length = 0x400', 1).replace(' .reset :', ' unsafe : > OTP, PAGE = 0\n .reset :') # Вводит потенциально необратимую загрузку данных в OTP. + self.assertIn('RESERVED_SECTION', [i['code'] for i in checked(text)[2]]) # Наличие резервного MEMORY не делает его записываемой RAM. + def test_flash_text_cannot_load_only_to_ram(self): # Рабочий после JTAG код не обязательно сохраняется при холодном Flash reset. + text = HELPER.generate('28335', 'flash').replace('.text : > FLASH_APP', '.text : > RAM_CODE') # Сохраняет корректные codestart и ramfuncs, но намеренно теряет постоянную копию основного кода. + self.assertIn('FLASH_LOAD', [i['code'] for i in checked(text)[2]]) # Flash-профиль не должен подтверждать такую карту полным успехом. + def test_copy_contract(self): # Flash RAM-функции требуют источника, назначения и количества слов. + text = HELPER.generate('28335', 'flash').replace('LOAD_SIZE(_RamfuncsLoadSize),', '').replace('LOAD_END(_RamfuncsLoadEnd),', '') # Удаляет только размер копирования, сохраняя остальные корректные назначения. + self.assertIn('RAMFUNCS_COPY', [i['code'] for i in checked(text)[2]]) # Отсутствие одного обязательного символа действительно обнаруживается. + def test_copy_end_alternative(self): # Исторический memcpy вправе вычислять размер как LoadEnd-LoadStart. + text = HELPER.generate('28335', 'flash').replace('LOAD_SIZE(_RamfuncsLoadSize),', '') # Оставляет действующие LOAD_END и оба начальных адреса. + self.assertEqual(checked(text)[2], []) # Не требует лишнего LOAD_SIZE у совместимых старых проектов TI. + def test_incomplete_preprocessor(self): # include и выражения не должны исчезать из итогового статуса. + text = '#include "board.cmd"\n' + HELPER.generate('28335', 'ram') # Неизвестный файл мог бы изменить физическую карту. + self.assertIn('PREPROCESSOR', [i['code'] for i in checked(text, boot='ram')[2]]) # Пользователь получает явно неполную проверку. + def test_unknown_syntax(self): # GROUP/UNION и split не интерпретируются ограниченным parser как обычный регион. + text = HELPER.generate('28335', 'ram').replace('.text : > RAM_CODE', '.text : >> RAM_CODE') # Требует поддерживаемого только TI разбиения секции. + self.assertIn('ADVANCED_SYNTAX', [i['code'] for i in checked(text, boot='ram')[2]]) # Поддерживаемая часть карты не даёт ложного полного успеха. + def test_unparsed_extra_blocks(self): # Грамматика не может молча проверить только начало сложного CMD. + text = HELPER.generate('28335', 'ram') # Использует заведомо корректную исходную карту. + extra = text + 'MEMORY { PAGE 1: OTHER : origin = 0x8000, length = 0x100 }' # Второй блок мог бы занять уже используемые физические слова. + self.assertIn('MULTIPLE_BLOCKS', [i['code'] for i in checked(extra, boot='ram')[2]]) # Даже численно понятный первый блок не даёт полного успеха. + prefix = text.replace('{ /* Начало выходных секций. */', '{ BROKEN_DIRECTIVE /* Начало выходных секций. */') # Вставляет неизвестный префикс, который не является телом предыдущей секции. + self.assertIn('SECTION_PREFIX', [i['code'] for i in checked(prefix, boot='ram')[2]]) # Произвольное начальное содержимое явно делает проверку неполной. + def test_no_overwrite(self): # Файл пользователя сохраняется без явно указанного --force. + with tempfile.TemporaryDirectory() as tmp: # Выделяет отдельный безопасный каталог для проверки записи. + target = Path(tmp) / 'board.cmd' # Существующий linker-файл не связан с рабочим проектом. + target.write_text('USER FILE', encoding='utf-8') # Контрольное содержимое должно остаться неизменным. + with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): # Не засоряет успешный вывод ожидаемой ошибкой CLI. + result = HELPER.main(['generate', '--device', '2812', '--boot', 'ram', '--output', str(target)]) # Проверяет настоящий режим x, а не предварительный exists. + self.assertEqual(result, 1) # Отказ перезаписи имеет ненулевой код. + self.assertEqual(target.read_text(encoding='utf-8'), 'USER FILE') # Пользовательские данные не изменены. + def test_unknown_map(self): # Произвольный лог не считается успешно проверенным MAP. + regions, sections, unused = checked(HELPER.generate('28335', 'ram'), boot='ram') # Даёт анализатору корректный исходный CMD. + issues, allocations = HELPER.inspect_map('not a TI map', regions, sections, '28335', 'ram') # Пробует заведомо чужой текстовый формат. + self.assertEqual(issues[0]['code'], 'MAP_FORMAT') # Отказ объясняет нужный формат TI. + + +class TiSmokeTest(unittest.TestCase): # Тестирует реальную грамматику и MAP TI вместо тестовых выдуманных адресов. + def test_real_ti_four_profiles(self): # Четыре настоящие COFF-сборки выполняются только с указанным установленным CGT. + if TI_ROOT is None: # Обычные stdlib-тесты остаются переносимыми без проприетарного компилятора. + self.skipTest('Для четырёх настоящих TI линковок задайте --ti .') # Пропуск явно виден в отчёте. + compiler = TI_ROOT / 'bin/cl2000.exe' # На Windows smoke использует согласованный пользователем установленный TI CGT. + self.assertTrue(compiler.is_file(), str(compiler)) # Не подменяет отсутствие компилятора успешной проверкой Python. + with tempfile.TemporaryDirectory(prefix='c2000-linker-ti-') as tmp: # Все объектные файлы и MAP находятся только во временном каталоге. + folder = Path(tmp) # Минимальная сборка не зависит от SDK или пользовательского проекта. + source = folder / 'fixture.c' # Исходник содержит типичные секции COFF обоих устройств. + source.write_text('volatile int observed; /* Создаёт изменяемую .ebss для проверки RAM. */\nconst int setting = 17; /* Создаёт .econst с числовой константой без аппаратного адреса. */\n#pragma CODE_SECTION(ram_old, "ramfuncs") /* Проверяет историческое имя секции RAM-кода. */\nint ram_old(void) { return observed + setting; } /* Даёт непустой LOAD/RUN для проверки размера копирования. */\n#pragma CODE_SECTION(ram_new, ".TI.ramfunc") /* Проверяет современную входную секцию в том же копируемом блоке. */\nint ram_new(void) { return observed + 2; } /* Не требует FPU32, аппаратных заголовков или RTS-арифметики. */\nint main(void) { observed = ram_old() + ram_new(); return 0; } /* Только проверяет линковку; этот тест никогда не запускается на MCU. */\n', encoding='utf-8') # Каждая строка fixture имеет осмысленный русский комментарий. + start = folder / 'start.asm' # Собственный минимальный branch исключает зависимость теста от TI SDK. + start.write_text(' .sect "codestart" ; Помещает проверочную двухсловную ветвь в аппаратный BEGIN.\n .global _boot_start ; Задаёт явную точку входа COFF для теста линкера.\n .ref _main ; Объявляет цель из минимального C-файла.\n_boot_start: LB _main ; Длинная ветвь занимает два слова C28x без настройки периферии.\n', encoding='utf-8') # Не выдаёт smoke branch за полноценный CodeStartBranch промышленного приложения. + for device in ('28335', '2812'): # Оба устройства собираются как C28x без FPU32. + for boot in ('ram', 'flash'): # Проверяет отдельный BEGIN, RAM и Flash LOAD/RUN. + with self.subTest(device=device, boot=boot): # Ошибка указывает конкретный профиль. + prefix = folder / (device + '_' + boot) # Разные OUT/MAP исключают использование предыдущей сборки. + cmd = prefix.with_suffix('.cmd') # Выбирает самостоятельную карту для данной линковки. + cmd.write_text(HELPER.generate(device, boot), encoding='utf-8') # В тест идёт именно пользовательский генератор. + subprocess.run([str(compiler), '--abi=coffabi', '-v28', '--float_support=softlib', '--compile_only', str(source), str(start)], cwd=folder, check=True, capture_output=True, text=True) # Настоящий TI компилирует оба fixture без FPU/SDK. + result = subprocess.run([str(compiler), '-v28', '--abi=coffabi', '--run_linker', '--rom_model', '--entry_point=_boot_start', '--stack_size=0x100', '--heap_size=0x40', '--output_file=' + str(prefix.with_suffix('.out')), '--map_file=' + str(prefix.with_suffix('.map')), str(folder / 'fixture.obj'), str(folder / 'start.obj'), str(cmd)], cwd=folder, capture_output=True, text=True) # TI проверяет секции, длины и синтаксис сгенерированной карты. + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) # Любая ошибка linker блокирует подтверждение профиля. + regions, sections, issues = checked(cmd.read_text(encoding='utf-8'), device, boot) # Проверяет ту же карту, что фактически получил TI. + extra, allocations = HELPER.inspect_map(prefix.with_suffix('.map').read_text(encoding='utf-8'), regions, sections, device, boot) # Разбирает настоящий, а не нарисованный под parser MAP. + self.assertEqual(issues + extra, [], str(prefix) + ': ' + str(issues + extra) + '; allocations=' + str([r.name for r in allocations]) + '; codestart=' + str(re.findall(r'codestart[^\n]*\n[^\n]*\n[^\n]*', prefix.with_suffix('.map').read_text(encoding='utf-8')))) # Подтверждает совместимость исходного CMD, LOAD/RUN и реальных copy-symbols TI. + self.assertTrue(allocations) # Пустой MAP-парсер не может дать положительный smoke. + print('TI PASS TMS320F' + device + ' ' + boot + ': COFF, codestart, data, ramfuncs, MAP/copy symbols') # Сохраняет короткое подтверждение каждого реального профиля. + +if __name__ == '__main__': # Импорт теста не запускает компилятор автоматически. + parser = argparse.ArgumentParser(description=__doc__) # Справка описывает явное подключение TI smoke. + parser.add_argument('--ti', type=Path, help='Каталог установленного TI C2000 CGT; включает 4 реальные линковки.') # Не скачивает компилятор и не требует платы. + arguments, unittest_args = parser.parse_known_args() # Допускает обычные флаги unittest вроде-v. + TI_ROOT = arguments.ti.resolve() if arguments.ti else None # Фиксирует согласованный путь перед выполнением тестов. + unittest.main(argv=[sys.argv[0], *unittest_args]) # Возвращает ненулевой код при любой Python/TI регрессии.