feat(firmware-build): Добавь сборку и паспорта проектов ПЛИС
Подключи ISE, Vivado и Quartus к общему итератору версии и HDL-паспорту. Проверяй новый bit/sof до создания manifest, SHA-256 и архива выпуска. Добавь установку локальных команд ISE, инструкции переноса и регрессионные проверки успешных и прерванных сборок.
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# CMD требует CRLF для надёжного чтения BAT с русскими комментариями в UTF-8.
|
||||
*.bat text eol=crlf
|
||||
@@ -0,0 +1,29 @@
|
||||
@chcp 65001 >nul
|
||||
@rem Переключение на UTF-8 нужно для русских комментариев и путей; установщик создаёт локальные файлы ISE.
|
||||
@echo off
|
||||
rem Сохраняет окружение и не раскрывает восклицательные знаки в путях проекта.
|
||||
setlocal EnableExtensions DisableDelayedExpansion
|
||||
rem Предпочитает штатный Windows launcher Python 3, если он установлен.
|
||||
where py >nul 2>nul
|
||||
rem При отсутствии launcher использует Python из PATH.
|
||||
if errorlevel 1 goto use_python
|
||||
rem Передаёт путь проекта или дополнительные параметры без изменения их порядка.
|
||||
py -3 "%~dp0ise_project.py" install %*
|
||||
rem Запоминает код установки до вывода подсказок и ожидания клавиши.
|
||||
set "FW_EXIT=%errorlevel%"
|
||||
rem Не запускает установку повторно через другой интерпретатор.
|
||||
goto done
|
||||
rem Начинает вариант для компьютеров без Windows launcher.
|
||||
:use_python
|
||||
rem При двойном щелчке Python запросит путь к проекту интерактивно.
|
||||
python "%~dp0ise_project.py" install %*
|
||||
rem Сохраняет исходный результат установщика.
|
||||
set "FW_EXIT=%errorlevel%"
|
||||
rem Завершает оба варианта запуска одинаково.
|
||||
:done
|
||||
rem Автоматические вызовы могут отключить ожидание переменной окружения.
|
||||
if defined FW_NO_PAUSE exit /b %FW_EXIT%
|
||||
rem Оставляет результат видимым при запуске двойным щелчком или перетаскиванием .xise.
|
||||
pause
|
||||
rem Передаёт вызывающей программе результат установки, а не команды pause.
|
||||
exit /b %FW_EXIT%
|
||||
@@ -0,0 +1,42 @@
|
||||
# Простое подключение контроля прошивки к ISE
|
||||
|
||||
1. Запустите **`INSTALL_ISE.bat` из этого каталога** и вставьте путь к вашему
|
||||
`.xise`. Можно просто перетащить `.xise` на этот BAT.
|
||||
2. Установщик сам прочитает кристалл, язык, имя `.bit` и рабочий каталог
|
||||
из проекта, скопирует локальные команды и создаст настройки.
|
||||
3. Один раз подключите паспорт к шине проекта по созданному файлу
|
||||
`firmware_identity_instance.txt`: адрес слова — `word_addr[3:0]`,
|
||||
данные — `word_data[15:0]`. Без использования выхода паспорт удалит синтезатор.
|
||||
4. Для сборки запускайте **`build_firmware.bat` рядом с `.xise`**.
|
||||
Для проверки настройки без сборки — `build_firmware.bat --check`.
|
||||
|
||||
Автоматически создаются:
|
||||
|
||||
| Файл | Назначение |
|
||||
|---|---|
|
||||
| `build_firmware.bat`, `firmware_build.py` | Локальная команда сборки |
|
||||
| `firmware_build.json` | Найденные пути проекта, библиотеки templates, `.bit` и окружения ISE |
|
||||
| `firmware_identity.json` | Изделие, кристалл и сохраняемый счётчик версии |
|
||||
| `firmware_identity_instance.txt` | Заготовка подключения для Verilog или VHDL |
|
||||
| `FIRMWARE_SETUP.md` | Памятка внутри проекта |
|
||||
| `build/ise/generated/` | Предварительно созданный HDL-паспорт; обновляется перед каждой сборкой |
|
||||
|
||||
Нужен Python 3.9+ через `py -3` или `python`. Окружение ISE ищется в PATH
|
||||
и стандартных каталогах Xilinx. Для нестандартной установки один раз задайте
|
||||
`ise_settings` в `firmware_build.json` — полный путь к `settings64.bat`
|
||||
или `settings32.bat`.
|
||||
|
||||
Общие инструменты используются из вашей библиотеки templates; вручную
|
||||
копировать её внутрь проекта не требуется. Если библиотека переехала,
|
||||
исправьте поле `templates` в `firmware_build.json`.
|
||||
|
||||
Первая сборка получает **0.1.1**. Установка и `--check` номер не расходуют;
|
||||
неудачная попытка синтеза расходует. Повторная установка сохраняет существующие
|
||||
JSON и версию; чужие BAT с тем же именем не перезаписывает.
|
||||
|
||||
Готовый `.bit`, manifest, SHA-256, HDL и метаданные сохраняются в
|
||||
`build/ise/firmware/HASH_ДАТА_ВРЕМЯ_UTC+3/`. После загрузки на плату сравните
|
||||
12 слов паспорта с `fpga.register_words` в manifest.
|
||||
|
||||
Установщик предназначен для FPGA-проектов с результатом `.bit`; CPLD с `.jed`
|
||||
требует отдельного шаблона. [Подробности паспорта и прямая Tcl-сборка](README.md).
|
||||
@@ -0,0 +1,241 @@
|
||||
# Контроль прошивок ПЛИС: Xilinx ISE, Vivado и Altera Quartus
|
||||
|
||||
Для ISE: запустите `INSTALL_ISE.bat`, укажите `.xise`, затем используйте
|
||||
созданный в проекте `build_firmware.bat`. [Краткая инструкция](ISE_PORTING.md).
|
||||
|
||||
Шаблон подключает к существующему проекту ПЛИС общий механизм
|
||||
[`firmware-info`](../../../c/firmware-info/README.md): сохраняемый итератор
|
||||
SemVer, время сборки в UTC, Git build ID, manifest, SHA-256 и архив выпуска.
|
||||
Параметры кристалла, выводы, частоты, ограничения и транспорт чтения регистров
|
||||
задаёт проект платы.
|
||||
|
||||
Цепочка: проект → Tcl своей САПР → `identity.py` из этого каталога → общий
|
||||
`c/firmware-info/tools/identity.py`. Итератор, правила Git и архивирования
|
||||
используются те же, что для CCS и Keil.
|
||||
|
||||
## Файлы и зависимости
|
||||
|
||||
| Файл | Назначение | Зависимости |
|
||||
|---|---|---|
|
||||
| `firmware_identity.example.json` | Начальная конфигурация изделия и счётчика | Копируется в проект |
|
||||
| `INSTALL_ISE.bat`, `ise_project.py` | Автоматическая установка локальных BAT, JSON, HDL и памятки из `.xise` | Windows, Python 3.9+ |
|
||||
| `ise-build.bat`, `ise_bootstrap.py`, `ise-project.md` | Шаблоны локальной команды сборки и памятки | Копируются установщиком в проект |
|
||||
| `identity.py` | Генерация Verilog-2001/VHDL-93 и контроль готового файла | Python 3.9+, stdlib, общий `identity.py` из templates |
|
||||
| `common.tcl` | Пути, язык, вызовы Python, обработка отказов | Tcl 8.5+ в САПР |
|
||||
| `ise.tcl` | Полная сборка `.xise` до `.bit` | Xilinx ISE / `xtclsh` |
|
||||
| `vivado.tcl` | Полная сборка `.xpr` до `.bit` | Vivado Project Mode |
|
||||
| `quartus.tcl` | Полная сборка `.qpf` до `.sof` | Altera/Intel Quartus II или Prime / `quartus_sh` |
|
||||
|
||||
Python 3 должен быть доступен как `python` из окружения САПР. Если ISE или
|
||||
Quartus подставляет старый Python, передайте путь к `python.exe` шестым
|
||||
аргументом. Git в PATH нужен для хешей; при недоступном Git получается
|
||||
явный `NOGIT000`, локальная сборка продолжается.
|
||||
|
||||
## 1. Конфигурация проекта
|
||||
|
||||
Скопируйте `firmware_identity.example.json` рядом с `.xise`, `.xpr` или `.qpf`
|
||||
под именем `firmware_identity.json`. Пример намеренно требует заменить имя
|
||||
кристалла; задайте своё изделие и точный part number.
|
||||
|
||||
| Поле | Смысл |
|
||||
|---|---|
|
||||
| `product` | Имя изделия, например `MY_FPGA_BOARD` |
|
||||
| `target` | Точное обозначение кристалла; информационное поле, настройки part в проекте не меняет |
|
||||
| `version` | Последний занятый номер `[major, minor, patch]`; в примере `[0, 1, 0]` |
|
||||
| `increment_version` | `true` включает общий сохраняемый итератор; `false` оставляет ручную версию |
|
||||
|
||||
Первая подготовка примера выдаёт **0.1.1**. Следующий pre-build выдаёт 0.1.2,
|
||||
включая попытки после ошибки синтеза. Переносы: 0.1.999 → 0.2.0,
|
||||
0.255.999 → 1.0.0. После 255.255.999 сборка останавливается. Post-build номер
|
||||
не увеличивает. Clean не сбрасывает конфигурацию.
|
||||
|
||||
Храните этот JSON в Git. Каталог `build/` добавьте в `.gitignore` проекта;
|
||||
файлы результатов САПР также должны игнорироваться, иначе они пометят сборку
|
||||
как dirty. Одна конфигурация JSON обслуживает только одну сборку одновременно.
|
||||
Для независимых ревизий платы используйте отдельные JSON и выходные каталоги.
|
||||
|
||||
## 2. Подключение паспорта к логике
|
||||
|
||||
Генератор создаёт `generated/firmware_identity.v` и
|
||||
`generated/firmware_identity.vhd`. Tcl добавляет в проект только выбранный
|
||||
язык. Паспорт — комбинационный ROM из 12 слов по 16 бит:
|
||||
|
||||
| Относительный адрес слова | Значение |
|
||||
|---|---|
|
||||
| 0 | Версия контракта: 1 |
|
||||
| 1, 2, 3 | Major, minor, patch |
|
||||
| 4 | Год, 2000..2199 |
|
||||
| 5 | Месяц в битах 15..8, день в 7..0 |
|
||||
| 6 | Час UTC в битах 15..8, минута в 7..0 |
|
||||
| 7 | Секунда UTC |
|
||||
| 8..11 | Восемь ASCII-символов build ID, первый символ пары в старшем октете |
|
||||
| 12..15 | Ноль |
|
||||
|
||||
Адреса относительные, без привязки к CPU, AXI, Avalon, SPI, Modbus или SETCAN.
|
||||
Базовый адрес в системе назначает проект. Это тот же порядок слов, что в
|
||||
`firmware_info_to_registers()`. Порядок байтов на конкретной шине задаёт
|
||||
адаптер проекта.
|
||||
|
||||
В верхний модуль Verilog добавьте экземпляр:
|
||||
|
||||
```verilog
|
||||
wire [15:0] identity_data; // 16-битное слово паспорта для мультиплексора чтения шины проекта.
|
||||
firmware_identity u_identity ( // Экземпляр генерируемого до синтеза паспорта текущей сборки.
|
||||
.word_addr(identity_word_offset), // Четыре младших бита относительного адреса слова в выделенном окне регистров.
|
||||
.word_data(identity_data) // Выход должен участвовать в ответе шины, чтобы синтезатор сохранил паспорт.
|
||||
); // Выбор окна и тактовое согласование выполняет логика проекта.
|
||||
```
|
||||
|
||||
Для VHDL объявите component в declarative part архитектуры и подключите его
|
||||
в теле архитектуры; component не требует заранее анализировать generated entity:
|
||||
|
||||
```vhdl
|
||||
component firmware_identity is -- Объявление размещается до begin архитектуры верхнего уровня.
|
||||
port ( -- Интерфейс совпадает с генерируемой entity firmware_identity.
|
||||
word_addr : in std_logic_vector(3 downto 0); -- Относительный адрес одного из 12 слов паспорта.
|
||||
word_data : out std_logic_vector(15 downto 0) -- Данные для ответа шины проекта.
|
||||
); -- Закрывает список портов компонента.
|
||||
end component; -- Привязка к entity из work выполняется при elaboration.
|
||||
signal identity_data : std_logic_vector(15 downto 0); -- Объявляется до begin и подключается к чтению регистров.
|
||||
-- Следующий экземпляр размещается после begin архитектуры верхнего уровня.
|
||||
u_identity : firmware_identity -- Создаёт экземпляр паспорта без примитивов производителя.
|
||||
port map ( -- Согласует имена сигналов проекта с интерфейсом компонента.
|
||||
word_addr => identity_word_offset, -- Четырёхбитный сигнал относительного адреса слова задаёт проект.
|
||||
word_data => identity_data -- Выход включается в мультиплексор ответа хосту.
|
||||
); -- Завершает подключение экземпляра.
|
||||
```
|
||||
|
||||
`identity_word_offset`, декодер окна и ответ шины реализуйте в проекте.
|
||||
Паспорт не имеет clock/reset, чтение комбинационное; при необходимости
|
||||
зарегистрируйте ответ в тактовом домене шины. Без подключения выхода синтезатор
|
||||
удалит паспорт. Смена языка существующего проекта требует удалить прежний
|
||||
generated-файл из списка исходников, чтобы не получить два одноимённых модуля.
|
||||
|
||||
## 3. Полная сборка
|
||||
|
||||
Команды запускаются **из корня проекта** в командной строке с настроенным
|
||||
окружением соответствующей САПР. В примерах templates подключён как
|
||||
`lib/templates`. Проект должен уже содержать свой top, part и UCF/XDC/QSF.
|
||||
Сценарии регистрируют generated-исходник в существующем проекте и выполняют
|
||||
полную цепочку до файла программирования.
|
||||
|
||||
Аргументы общие: `project config output image verilog|vhdl [python]`.
|
||||
Пути разрешаются относительно начального рабочего каталога. `image` — точный
|
||||
путь результата вашей САПР, а `output` — отдельный каталог паспорта и архивов;
|
||||
передача `output` не меняет настройки выходного каталога САПР.
|
||||
|
||||
**ISE:**
|
||||
|
||||
```bat
|
||||
rem Собирает существующий ISE-проект, в котором BitGen создаёт top.bit рядом с .xise.
|
||||
xtclsh "lib/templates/tools/firmware-build/fpga/ise.tcl" "board.xise" "firmware_identity.json" "build/ise" "top.bit" verilog
|
||||
```
|
||||
|
||||
Используется `Generate Programming File -force rerun_all`: XST, реализация
|
||||
и BitGen выполняются заново. Логический `false` от процесса считается ошибкой.
|
||||
|
||||
**Vivado:**
|
||||
|
||||
```bat
|
||||
rem Замените board.runs/impl_1/top.bit реальным путём .bit выбранного implementation run.
|
||||
vivado -mode batch -source "lib/templates/tools/firmware-build/fpga/vivado.tcl" -tclargs "board.xpr" "firmware_identity.json" "build/vivado" "board.runs/impl_1/top.bit" verilog
|
||||
```
|
||||
|
||||
Берётся активный implementation run и его родительский synthesis run;
|
||||
нестандартные имена runs поддерживаются. Оба запуска сбрасываются, затем
|
||||
выполняются синтез и реализация до `write_bitstream`. После `wait_on_run`
|
||||
отдельно проверяется `PROGRESS`, поэтому завершение с ошибкой не вызывает упаковку.
|
||||
|
||||
**Altera / Intel Quartus:**
|
||||
|
||||
```bat
|
||||
rem Собирает текущую revision из .qpf; image должен совпадать с результатом Assembler.
|
||||
quartus_sh -t "lib/templates/tools/firmware-build/fpga/quartus.tcl" "board.qpf" "firmware_identity.json" "build/quartus" "output_files/board.sof" vhdl
|
||||
```
|
||||
|
||||
`execute_flow -compile` запускает полную компиляцию с Assembler.
|
||||
Для другой revision передайте седьмой аргумент после пути к Python:
|
||||
|
||||
```bat
|
||||
rem Укажите реальный Python 3 и отдельный JSON счётчика ревизии B.
|
||||
quartus_sh -t "lib/templates/tools/firmware-build/fpga/quartus.tcl" "board.qpf" "identity_B.json" "build/quartus_B" "output_files/board_B.sof" vhdl "C:/Python313/python.exe" "board_B"
|
||||
```
|
||||
|
||||
Не добавляйте второе увеличение версии в существующие pre-flow/pre-synthesis hooks.
|
||||
Эти Tcl-файлы предназначены для запуска полной сборки из командной строки;
|
||||
их нельзя подключать как вложенные hooks этапов синтеза. Для выпуска через
|
||||
данную схему запускайте указанный сценарий, в том числе после работы в GUI.
|
||||
|
||||
## 4. Результаты и проверка
|
||||
|
||||
```text
|
||||
build/vivado/
|
||||
generated/
|
||||
firmware_identity.h
|
||||
firmware_identity.v
|
||||
firmware_identity.vhd
|
||||
firmware_identity.json
|
||||
firmware/
|
||||
HASH_YYYY-MM-DD_HH-MM-SS_UTC+3/
|
||||
top.bit
|
||||
top.bit.manifest.json
|
||||
top.bit.sha256
|
||||
firmware_identity.v
|
||||
firmware_identity.json
|
||||
```
|
||||
|
||||
Для VHDL архив содержит выбранный `.vhd`, для Quartus — `.sof` вместо `.bit`.
|
||||
Manifest и `.sha256` также создаются рядом с исходным файлом программирования.
|
||||
Хеш и дата архивной папки берутся из pre-build, дата имени переводится в UTC+3;
|
||||
в самом паспорте остаётся UTC. Знак `+` у build ID означает изменённые исходники
|
||||
или templates. `firmware_identity.h` — сопутствующий результат общего генератора;
|
||||
FPGA-сценарии используют HDL.
|
||||
|
||||
Упаковка отклоняет отсутствующий, пустой или не обновившийся после prepare
|
||||
файл. Проверяется содержимое и время записи: повторная запись тех же байтов
|
||||
допускается. Это защита от старого результата; она не проверяет наличие паспорта
|
||||
внутри vendor bitstream. Правильность подключения подтверждается чтением всех
|
||||
12 регистров с работающей ПЛИС и сравнением с `fpga.register_words` в manifest.
|
||||
SHA-256 относится к файлу программатора целиком и хранится снаружи образа.
|
||||
|
||||
Проверить сохранённый файл можно общим инструментом:
|
||||
|
||||
```bat
|
||||
rem Проверяет размер и SHA-256 указанного файла по созданному рядом manifest.
|
||||
python "lib/templates/c/firmware-info/tools/identity.py" verify --image "board.runs/impl_1/top.bit" --manifest "board.runs/impl_1/top.bit.manifest.json"
|
||||
```
|
||||
|
||||
Для локального экспорта папки выпуска повторите только finish с `--export`;
|
||||
номер версии при этом не меняется:
|
||||
|
||||
```bat
|
||||
rem Копирует текущую архивную папку и проверяет SHA-256 экспортной копии.
|
||||
python "lib/templates/tools/firmware-build/fpga/identity.py" finish --metadata "build/vivado/generated/firmware_identity.json" --output "build/vivado/firmware" --export "D:/Firmware/FPGA"
|
||||
```
|
||||
|
||||
Шаблон выпускает `.bit` и `.sof`. Преобразование в `.mcs`, `.bin`, `.pof` или
|
||||
`.rbf`, загрузка на плату и публикация в сетевую базу требуют отдельного
|
||||
проектного этапа. Контрольная сумма нативного образа не переносится на
|
||||
преобразованный файл: для него создайте отдельные seal/verify общим инструментом.
|
||||
|
||||
## Проверки и состояние
|
||||
|
||||
Хостовые тесты проверяют эталон всех 12 слов, совпадение Verilog/VHDL,
|
||||
итератор, свежесть результата, повреждение SHA-256, архив и экспорт. Реальные
|
||||
Tcl-сценарии исполняются в Tcl из Python tkinter с заменой API производителей;
|
||||
Python-генератор и упаковка выполняются полностью. Нужен Python с tkinter/Tcl,
|
||||
графическое окно не открывается.
|
||||
|
||||
```bat
|
||||
rem Запускает проверки адаптера и управляющих сценариев без лицензии САПР.
|
||||
python -m unittest discover -s tools/firmware-build/fpga/tests -v
|
||||
```
|
||||
|
||||
Шаблон пока не подключён к конкретной плате. Полный синтез и чтение паспорта
|
||||
на железе требуют установленной САПР, проекта и платы; хостовые тесты эти
|
||||
проверки не заменяют.
|
||||
|
||||
Команды сверены с руководствами производителей:
|
||||
[ISE Command Line Tools UG628](https://docs.amd.com/v/u/en-US/devref),
|
||||
[Vivado: запуск синтеза и реализации UG939](https://docs.amd.com/r/2023.1-English/ug939-vivado-designing-with-ip-tutorial/Step-8-Launch-Synthesis-and-Implementation),
|
||||
[Quartus: полный Tcl flow](https://www.intel.com/content/www/us/en/docs/programmable/683432/22-1/the-tcl-shell-in-interactive-mode-example.html).
|
||||
@@ -0,0 +1,76 @@
|
||||
# Общие процедуры совместимы с Tcl 8.5 старых ISE; пути передаются списками без eval.
|
||||
namespace eval fw {
|
||||
# Определяет Python-инструмент относительно поставки, а не текущего каталога САПР.
|
||||
variable helper [file normalize [file join [file dirname [info script]] identity.py]]
|
||||
# Закрывает пространство имён, не меняя глобальные команды САПР.
|
||||
}
|
||||
# Проверяет общие аргументы до открытия проекта и резервирования версии.
|
||||
proc fw::init {arguments toolchain} {
|
||||
# Объявляет настройки сценария в общем пространстве имён.
|
||||
variable settings
|
||||
# Пять обязательных аргументов и необязательный путь к python.exe.
|
||||
if {[llength $arguments] < 5 || [llength $arguments] > 6} {
|
||||
# Неверная команда не должна расходовать номер версии.
|
||||
error "Usage: project config output image verilog|vhdl ?python?"
|
||||
# Завершает проверку количества аргументов.
|
||||
}
|
||||
# Имена полей определяют одинаковый порядок аргументов всех трёх сценариев.
|
||||
foreach name {project config output image} value [lrange $arguments 0 3] {
|
||||
# Нормализует пути до перехода в каталог проекта.
|
||||
set settings($name) [file normalize $value]
|
||||
# Заканчивает обработку четырёх путей.
|
||||
}
|
||||
# Сохраняет выбранный HDL, чтобы в проект не попали две реализации одного модуля.
|
||||
set settings(language) [lindex $arguments 4]
|
||||
# Принимает только языки, поддержанные генератором паспорта.
|
||||
if {[lsearch -exact {verilog vhdl} $settings(language)] < 0} {
|
||||
# Останавливает сборку при опечатке в языке.
|
||||
error "Expected verilog or vhdl"
|
||||
# Заканчивает проверку выбранного языка.
|
||||
}
|
||||
# По умолчанию использует Python 3 из PATH, без аргументов командной оболочки.
|
||||
set settings(python) python
|
||||
# Полный путь позволяет обойти Python старой версии, встроенный в САПР.
|
||||
if {[llength $arguments] == 6} {
|
||||
# Делает путь python.exe абсолютным без раскрытия Windows junction до последующего cd.
|
||||
set settings(python) [file join [pwd] [lindex $arguments 5]]
|
||||
# Завершает выбор интерпретатора.
|
||||
}
|
||||
# Передаёт генератору имя формата нативного результата.
|
||||
set settings(toolchain) $toolchain
|
||||
# Разделяет генерируемые исходники и архивы внутри каталога конфигурации.
|
||||
set settings(generated) [file join $settings(output) generated]
|
||||
# Выбирает расширение только одного подключаемого HDL-файла.
|
||||
set extension [expr {$settings(language) eq "verilog" ? "v" : "vhd"}]
|
||||
# Имя модуля совпадает в Verilog и VHDL и известно верхнему уровню проекта.
|
||||
set settings(hdl) [file join $settings(generated) firmware_identity.$extension]
|
||||
# Проверяет наличие проектного файла до запуска САПР.
|
||||
if {![file isfile $settings(project)] || ![file isfile $settings(config)]} {
|
||||
# JSON создаётся пользователем из примера, чтобы не выпустить изделие с чужим именем.
|
||||
error "Project or firmware identity config does not exist"
|
||||
# Завершает проверку обязательных файлов.
|
||||
}
|
||||
# Зависимые исходники и ограничения проекта разрешаются относительно его каталога.
|
||||
cd [file dirname $settings(project)]
|
||||
# Заканчивает инициализацию путей и языка.
|
||||
}
|
||||
# Резервирует версию один раз до любых шагов синтеза.
|
||||
proc fw::prepare {} {
|
||||
# Получает проверенные аргументы текущего запуска.
|
||||
variable settings
|
||||
# Получает абсолютный путь общего генератора.
|
||||
variable helper
|
||||
# exec передаёт каждый путь отдельно; stderr остаётся в журнале САПР, ненулевой код вызывает Tcl error.
|
||||
puts [exec $settings(python) $helper prepare --repository [file dirname $settings(project)] --config $settings(config) --output $settings(generated) --image $settings(image) --toolchain $settings(toolchain) --language $settings(language) 2>@stderr]
|
||||
# Завершает подготовку паспорта.
|
||||
}
|
||||
# Упаковывает результат только после успешного возврата полного flow.
|
||||
proc fw::finish {} {
|
||||
# Использует те же пути, что и pre-build данного запуска.
|
||||
variable settings
|
||||
# Выбирает тот же Python-инструмент, который создавал паспорт.
|
||||
variable helper
|
||||
# Генератор проверяет обновление файла, SHA-256 и архивную копию; ошибка блокирует успешное завершение Tcl.
|
||||
puts [exec $settings(python) $helper finish --metadata [file join $settings(generated) firmware_identity.json] --output [file join $settings(output) firmware] 2>@stderr]
|
||||
# Завершает контроль готового образа.
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"product": "MY_FPGA_BOARD",
|
||||
"target": "REPLACE_WITH_EXACT_FPGA_PART",
|
||||
"version": [0, 1, 0],
|
||||
"increment_version": true
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
||||
@chcp 65001 >nul
|
||||
@rem Переключение на UTF-8 сохраняет русские пути и комментарии локальной команды сборки ISE.
|
||||
@echo off
|
||||
rem Не допускает изменения окружения родительской консоли и раскрытия символа ! в путях.
|
||||
setlocal EnableExtensions DisableDelayedExpansion
|
||||
rem Находит штатный launcher Python 3 без привязки к каталогу его установки.
|
||||
where py >nul 2>nul
|
||||
rem При отсутствии launcher переключается на Python из PATH.
|
||||
if errorlevel 1 goto use_python
|
||||
rem Запускает локальную обвязку независимо от текущего рабочего каталога.
|
||||
py -3 "%~dp0firmware_build.py" %*
|
||||
rem Возвращает точный код сборки или проверки параметров.
|
||||
exit /b %errorlevel%
|
||||
rem Обслуживает компьютеры, где установлен только python.exe.
|
||||
:use_python
|
||||
rem Передаёт необязательный --check для проверки настройки без увеличения версии.
|
||||
python "%~dp0firmware_build.py" %*
|
||||
rem Не скрывает ошибку отсутствующего Python, окружения ISE или самой сборки.
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,8 @@
|
||||
@rem Получает окружение ISE в дочернем CMD; вывод SET захватывается Python и не попадает в журнал.
|
||||
@echo off
|
||||
rem Активирует штатную установку, путь передан через окружение без вставки в shell-код.
|
||||
call "%FW_ISE_SETTINGS%" >nul
|
||||
rem Не возвращает частично настроенное окружение после отказа settings.bat.
|
||||
if errorlevel 1 exit /b %errorlevel%
|
||||
rem CMD /u выдаёт переменные в UTF-16LE независимо от текущей кодовой страницы.
|
||||
set
|
||||
@@ -0,0 +1,27 @@
|
||||
# Сборка прошивки ISE
|
||||
|
||||
Для сборки запускайте `build_firmware.bat`. Версия увеличивается перед
|
||||
синтезом; результат и архив сохраняются в `build/ise/firmware/`.
|
||||
`build_firmware.bat --check` проверяет настройки и наличие ISE без сборки
|
||||
и увеличения версии.
|
||||
|
||||
Один раз подключите экземпляр `firmware_identity` к верхнему модулю:
|
||||
адрес слова — `word_addr[3:0]`, выход — `word_data[15:0]`.
|
||||
Заготовка подключения находится в `firmware_identity_instance.txt`.
|
||||
Выход включите в чтение вашей шины, иначе синтезатор удалит паспорт.
|
||||
Сценарий сам добавит generated HDL к проекту при сборке.
|
||||
|
||||
В `firmware_identity.json` меняются изделие и номер версии.
|
||||
В `firmware_build.json` находятся пути проекта, `.bit`, templates,
|
||||
каталог результатов, язык и необязательный путь `ise_settings` к
|
||||
`settings64.bat` или `settings32.bat` вашей установки ISE.
|
||||
Если окружение ISE не найдено автоматически, задайте этот путь один раз.
|
||||
|
||||
Общие инструменты используются из каталога templates, указанного в настройках.
|
||||
Храните локальные BAT, Python, JSON и заготовку подключения в Git.
|
||||
Каталог сборки добавлен установщиком в `.gitignore`; остальные результаты
|
||||
ISE также добавьте в игнорируемые файлы проекта.
|
||||
|
||||
После загрузки `.bit` сравните 12 слов паспорта с `fpga.register_words`
|
||||
в manifest из архива. Повторная установка сохраняет существующие JSON
|
||||
и занятый номер версии. Одновременные сборки с одним JSON не поддерживаются.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Подключает общие процедуры из того же каталога поставки templates.
|
||||
source [file join [file dirname [info script]] common.tcl]
|
||||
# Любая ошибка подготовки, XST, реализации или упаковки возвращает ненулевой код xtclsh.
|
||||
if {[catch {
|
||||
# Принимает project.xise, config.json, output, top.bit, язык и необязательный python.exe.
|
||||
fw::init $argv ise
|
||||
# Открывает существующий проект с выбранной платой, top и ограничениями UCF.
|
||||
project open $fw::settings(project)
|
||||
# Создаёт HDL-паспорт до синтеза XST и увеличивает только общий счётчик.
|
||||
fw::prepare
|
||||
# Добавляет выбранный HDL-паспорт; повторное добавление существующего источника поддерживает ISE.
|
||||
xfile add $fw::settings(hdl)
|
||||
# Принудительно повторяет всю цепочку до BitGen, чтобы паспорт вошёл в новый bitstream.
|
||||
set result [process run "Generate Programming File" -force rerun_all]
|
||||
# ISE может вернуть false без Tcl-исключения при отказе дочернего процесса.
|
||||
if {![string is true -strict $result]} {
|
||||
# Не позволяет упаковать оставшийся от предыдущей сборки файл .bit.
|
||||
error "ISE Generate Programming File failed: $result"
|
||||
# Заканчивает проверку результата процесса.
|
||||
}
|
||||
# Записывает manifest, SHA-256 и архив только после успешного BitGen.
|
||||
fw::finish
|
||||
# Сохраняет регистрацию HDL-паспорта и закрывает проект.
|
||||
project close
|
||||
# catch сохраняет текст ошибки, не продолжая цепочку сборки.
|
||||
} message]} {
|
||||
# Выводит причину отказа в поток ошибок командной строки.
|
||||
puts stderr $message
|
||||
# Ненулевой код позволяет внешнему BAT или CI остановить выпуск.
|
||||
exit 1
|
||||
# Закрывает обработчик ошибок.
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"""Локальная обвязка ISE: настройки проекта и запуск общего инструмента templates.""" # Установщик копирует файл как firmware_build.py.
|
||||
import json # Читает только настройки путей, оставляя номер версии отдельному JSON.
|
||||
from pathlib import Path # Разрешает пути относительно установленной обвязки.
|
||||
import subprocess # Передаёт аргументы отдельным списком без сборки shell-команды.
|
||||
import sys # Сохраняет выбранный Python 3 и код возврата общей сборки.
|
||||
|
||||
|
||||
def main(): # Одна локальная команда обращается к алгоритмам общей библиотеки.
|
||||
config = Path(__file__).resolve().with_name("firmware_build.json") # Не зависит от того, из какой папки запущен BAT.
|
||||
settings = json.loads(config.read_text(encoding="utf-8-sig")) # Принимает UTF-8, включая JSON, сохранённый редактором с BOM.
|
||||
templates = (config.parent / settings["templates"]).resolve() # Поддерживает относительное подключение или абсолютный путь библиотеки.
|
||||
helper = templates / "tools/firmware-build/fpga/ise_project.py" # Хранит реализацию сборки в одном месте для всех проектов.
|
||||
if not helper.is_file(): # Перемещение библиотеки должно дать понятную причину отказа.
|
||||
raise ValueError(f"templates not found; edit templates in {config}") # Не запускает сборку с неподходящим инструментом из PATH.
|
||||
return subprocess.call([sys.executable, str(helper), "build", "--settings", str(config), *sys.argv[1:]]) # Сохраняет пробелы и кириллицу в каждом аргументе.
|
||||
|
||||
|
||||
if __name__ == "__main__": # Импорт для проверки не должен запускать сборку проекта.
|
||||
try: # Превращает ошибку настройки в короткое сообщение для консоли.
|
||||
sys.exit(main()) # Сохраняет точный код завершения общего Tcl-flow.
|
||||
except (OSError, ValueError, KeyError) as error: # Обрабатывает отсутствующие файлы и неверный JSON до запуска САПР.
|
||||
print(f"[firmware-build] {error}", file=sys.stderr) # Не скрывает путь настройки, которую требуется исправить.
|
||||
sys.exit(2) # Отличает ошибку настройки от успешной сборки.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,60 @@
|
||||
# Подключает общие пути и Python-этапы, совместимые с Quartus II и Prime.
|
||||
source [file join [file dirname [info script]] common.tcl]
|
||||
# Ошибка execute_flow или упаковки не должна выглядеть успешным выпуском.
|
||||
if {[catch {
|
||||
# Берёт аргументы quartus_sh из штатного массива Quartus, включая необязательную revision.
|
||||
set arguments $quartus(args)
|
||||
# По умолчанию project_open выбирает текущую revision из .qpf.
|
||||
set revision ""
|
||||
# Седьмой аргумент разрешён только после явного пути python.exe.
|
||||
if {[llength $arguments] == 7} {
|
||||
# Позволяет явно собрать ревизию платы вместо текущей из .qpf.
|
||||
set revision [lindex $arguments 6]
|
||||
# Передаёт общему разбору только его шесть аргументов.
|
||||
set arguments [lrange $arguments 0 5]
|
||||
# Завершает извлечение необязательной revision.
|
||||
}
|
||||
# Принимает project.qpf, config.json, output, revision.sof, язык и необязательный python.exe.
|
||||
fw::init $arguments quartus
|
||||
# Подключает штатные команды открытия проекта.
|
||||
package require ::quartus::project
|
||||
# Загружает полную цепочку Analysis/Synthesis, Fitter, Assembler и Timing Analyzer.
|
||||
load_package flow
|
||||
# Quartus принимает имя проекта без расширения .qpf.
|
||||
set project_name [file rootname $fw::settings(project)]
|
||||
# Явная revision нужна для нескольких аппаратных конфигураций одного проекта.
|
||||
if {$revision eq ""} {
|
||||
# Использует текущую ревизию проекта, сохранённую в .qpf.
|
||||
project_open $project_name
|
||||
# При наличии имени выбирает заданную ревизию.
|
||||
} else {
|
||||
# Раздельный аргумент сохраняет пробелы в имени ревизии.
|
||||
project_open -revision $revision $project_name
|
||||
# Завершает открытие проекта.
|
||||
}
|
||||
# Генерирует новый паспорт перед Analysis/Synthesis, используя общий итератор.
|
||||
fw::prepare
|
||||
# По умолчанию регистрирует Verilog-2001 для старых Quartus II.
|
||||
set assignment VERILOG_FILE
|
||||
# VHDL-проект получает только альтернативный .vhd файл.
|
||||
if {$fw::settings(language) eq "vhdl"} {
|
||||
# Штатное назначение Quartus определяет язык нового исходника.
|
||||
set assignment VHDL_FILE
|
||||
# Завершает выбор языка.
|
||||
}
|
||||
# Добавляет паспорт к исходникам выбранной ревизии проекта.
|
||||
set_global_assignment -name $assignment $fw::settings(hdl)
|
||||
# Полная компиляция экспортирует назначения перед запуском модулей и создаёт новый .sof.
|
||||
execute_flow -compile
|
||||
# Контролирует свежесть .sof, формирует SHA-256, manifest и архив.
|
||||
fw::finish
|
||||
# Закрывает проект после успешной упаковки результата.
|
||||
project_close
|
||||
# Перехватывает отказ САПР и сохраняет причину для внешней сборки.
|
||||
} message]} {
|
||||
# Пишет сообщение в стандартный поток ошибок.
|
||||
puts stderr $message
|
||||
# Не допускает успешный exit при ошибке компиляции или проверки образа.
|
||||
exit 1
|
||||
# Завершает общий обработчик ошибок.
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Исполняет реальные Tcl-сценарии с заменой только команд производителей САПР.""" # Проверяет порядок, пути с пробелами и блокирование post-build.
|
||||
import json # Проверяет, сколько номеров занял каждый запуск сценария.
|
||||
import os # Временно задаёт путь минимальной инициализации Tcl без GUI.
|
||||
from pathlib import Path # Находит сценарии относительно тестов.
|
||||
import sys # Передаёт Tcl путь к тому же Python 3, которым выполняется тест.
|
||||
import tempfile # Изолирует проекты и готовые образы каждого запуска.
|
||||
import unittest # Организует успешные и ошибочные сценарии трёх САПР.
|
||||
import tkinter # Предоставляет настоящий Tcl-интерпретатор без открытия окна Tk.
|
||||
SCRIPTS = Path(__file__).resolve().parents[1] # Находит поставку FPGA-сценариев.
|
||||
|
||||
|
||||
class FlowTests(unittest.TestCase): # Проверяет Tcl-управление без подмены генерации паспорта и архивирования.
|
||||
def run_flow(self, toolchain, language="verilog", failure="", revision=""): # Исполняет один полный flow во временном проекте.
|
||||
scratch = SCRIPTS.parents[2] / ".codex-build" # Каталог внутри репозитория исключает внешние junction AppData при проверке Tcl на Windows.
|
||||
scratch.mkdir(exist_ok=True) # Хранит временные проекты в уже игнорируемой области сборки.
|
||||
with tempfile.TemporaryDirectory(prefix="FPGA flow пробел ", dir=scratch) as temporary: # Кириллица и пробелы должны пережить Tcl exec и Python CLI.
|
||||
root = Path(temporary) # Не использует настоящие проекты или настройки САПР.
|
||||
project = root / {"ise": "board.xise", "vivado": "board.xpr", "quartus": "board.qpf"}[toolchain] # Создаёт только входной маркер нужного типа проекта.
|
||||
project.touch() # Открытие моделируется командами производителя, наличие проверяет реальный common.tcl.
|
||||
config = root / "identity.json" # Хранит реальный сохраняемый счётчик.
|
||||
config.write_text(json.dumps({"product": "Плата", "target": "test", "version": [0, 1, 0], "increment_version": "true" if failure == "prepare" else True}), encoding="utf-8") # Ошибка типа флага должна остановить prepare до резервирования; штатный flow занимает 0.1.1.
|
||||
image = root / ("board.sof" if toolchain == "quartus" else "board.bit") # Совпадает с нативным форматом сценария.
|
||||
image.write_bytes(b"old image") # Позволяет проверить отказ упаковки старого файла после сбоя сборки.
|
||||
previous = Path.cwd() # Настоящий Tcl cd меняет текущий каталог процесса Python.
|
||||
script = os.path.relpath(SCRIPTS / (toolchain + ".tcl"), previous) # Относительный путь проверяет нахождение Python-helper после перехода в другой каталог проекта.
|
||||
interpreter = tkinter.Tcl() # Выполняет установленный Tcl без окна и без запуска xtclsh/Vivado/Quartus.
|
||||
calls = [] # Собирает последовательность только вендорных команд.
|
||||
def vendor(name, *arguments): # Моделирует интерфейс САПР, сохраняя настоящий exec Python-инструмента.
|
||||
calls.append((name, *arguments)) # Позволяет проверить порядок подготовки и запуска дочерних шагов.
|
||||
if name == "current_run": # В Vivado выбирается активная реализация с нестандартным именем.
|
||||
return "impl_board" # Проверяет отсутствие жёсткой привязки к impl_1.
|
||||
if name == "get_runs": # Возвращает имя объекта run без лишних преобразований.
|
||||
return arguments[-1] # Достаточно для чтения PARENT, SRCSET и PROGRESS.
|
||||
if name == "get_property": # Моделирует свойства выбранного синтеза и реализации.
|
||||
field, target = arguments # Сохраняет различие родительского синтеза и активного в GUI.
|
||||
if field == "PARENT": # Реализация может зависеть от другого синтеза.
|
||||
return "synth_board" # Сценарий должен выбрать этого родителя.
|
||||
if field == "SRCSET": # Новый модуль должен попасть именно в исходники родителя реализации.
|
||||
return "sources_board" # Проверяет выбор набора вместо предположения sources_1.
|
||||
return "0%" if failure == target else "100%" # Ожидание завершения при ошибке не считается успехом.
|
||||
finished = name == "execute_flow" or name == "process" or (name == "wait_on_run" and arguments[0] == "impl_board") # Определяет место записи результата компиляции.
|
||||
if finished and not failure: # Только успешный flow создаёт новый файл.
|
||||
image.write_bytes(b"new image from " + toolchain.encode("ascii")) # Имитирует точные байты нового .bit/.sof.
|
||||
if name == "process": # ISE сообщает отказ логическим результатом без Tcl error.
|
||||
return "false" if failure else "true" # Проверяет особую обработку boolean в ise.tcl.
|
||||
if name == "execute_flow" and failure: # Quartus сообщает ошибку компиляции Tcl-исключением.
|
||||
raise RuntimeError("test compiler failure") # Вызывает catch в настоящем quartus.tcl.
|
||||
return "" # Остальные команды производителя успешно завершаются без значения.
|
||||
for name in ("project", "xfile", "process", "open_project", "current_run", "get_property", "get_runs", "add_files", "update_compile_order", "reset_run", "launch_runs", "wait_on_run", "close_project", "load_package", "project_open", "set_global_assignment", "execute_flow", "project_close"): # Подменяет только API производителя, не команды ядра Tcl.
|
||||
interpreter.createcommand(name, lambda *arguments, name=name: vendor(name, *arguments)) # Привязывает имя при создании каждой callback-функции.
|
||||
interpreter.eval('proc exit {code} {error "flow-exit:$code"}') # Не позволяет Tcl exit завершить процесс тестов.
|
||||
interpreter.call("package", "provide", "::quartus::project", "1.0") # Удовлетворяет require пакета, чьи команды заменены заглушками.
|
||||
arguments = [str(project), str(config), str(root / "build"), str(image), language, sys.executable] # Передаёт пути списком, без shell-экранирования.
|
||||
if revision: # Quartus допускает дополнительное имя аппаратной ревизии.
|
||||
arguments.append(revision) # Проверяет передачу значения с пробелами одним аргументом.
|
||||
interpreter.call("set", "quartus(args)" if toolchain == "quartus" else "argv", tuple(arguments)) # Создаёт штатный источник аргументов соответствующей САПР.
|
||||
try: # Гарантирует восстановление cwd даже при ожидаемом exit 1.
|
||||
if failure: # Неудачная компиляция должна прервать сценарий до упаковки.
|
||||
with self.assertRaisesRegex(tkinter.TclError, "flow-exit:1"): # Проверяет внешнее состояние отказа.
|
||||
interpreter.call("source", script) # Исполняет настоящий сценарий со всеми проверками.
|
||||
else: # Успешный сценарий обязан дойти до архива.
|
||||
interpreter.call("source", script) # Реальный exec создаёт HDL, manifest и архив.
|
||||
self.assertEqual(json.loads(config.read_text(encoding="utf-8"))["version"], [0, 1, 0] if failure == "prepare" else [0, 1, 1]) # Ошибка конфигурации не занимает номер; после начала сборки номер резервируется ровно один раз.
|
||||
manifests = list((root / "build/firmware").glob("*/*.manifest.json")) # Ищет только выпущенные архивы текущего запуска.
|
||||
self.assertEqual(len(manifests), 0 if failure else 1) # Ошибка синтеза/реализации не должна выпустить старый образ.
|
||||
if failure == "prepare": # Ошибка общего Python pre-build должна блокировать запуск САПР.
|
||||
self.assertFalse(any(call[0] in ("process", "launch_runs", "execute_flow") for call in calls)) # Не допускает синтез с прежним паспортом после ошибки настроек.
|
||||
if not failure: # Дополнительно проверяет выбранный HDL после реального prepare.
|
||||
suffix = "v" if language == "verilog" else "vhd" # В проект регистрируется только выбранный вариант.
|
||||
self.assertTrue(any(str(root / "build/generated" / ("firmware_identity." + suffix)).replace("\\", "/") in str(call).replace("\\\\", "/") for call in calls)) # Учитывает Windows-разделители в строковом представлении кортежей.
|
||||
finally: # Tcl cd не должен влиять на следующий тест или временную очистку.
|
||||
os.chdir(previous) # Возвращает исходный каталог процесса до удаления проекта.
|
||||
return calls # Отдаёт последовательность команд для специфических проверок САПР.
|
||||
|
||||
def test_all_toolchains_and_languages(self): # Каждый сценарий должен выпустить файл для обеих HDL-реализаций.
|
||||
for toolchain in ("ise", "vivado", "quartus"): # Покрывает три независимых набора Tcl-команд.
|
||||
for language in ("verilog", "vhdl"): # Проверяет правильное расширение и тип назначения исходника.
|
||||
with self.subTest(toolchain=toolchain, language=language): # Показывает точную конфигурацию возможного отказа.
|
||||
self.run_flow(toolchain, language) # Создаёт реальный паспорт и архив без лицензии производителя.
|
||||
|
||||
def test_failures_do_not_archive_old_images(self): # Проверяет false ISE, ошибку Quartus и неуспешные runs Vivado.
|
||||
for toolchain, failure in (("ise", "compile"), ("quartus", "compile"), ("vivado", "synth_board"), ("vivado", "impl_board"), ("ise", "prepare"), ("vivado", "prepare"), ("quartus", "prepare")): # Отдельно покрывает обе стадии Vivado и отказ Python pre-build во всех САПР.
|
||||
with self.subTest(toolchain=toolchain, failure=failure): # Сохраняет контекст ожидаемой ошибки.
|
||||
self.run_flow(toolchain, failure=failure) # Старый файл существует, но архив не должен появиться.
|
||||
|
||||
def test_revision_and_parent_synthesis(self): # Проверяет нестандартные имена конфигураций проектов.
|
||||
quartus = self.run_flow("quartus", revision="board revision B") # Явная ревизия содержит пробелы и передаётся единым аргументом.
|
||||
self.assertTrue(any(call[:3] == ("project_open", "-revision", "board revision B") for call in quartus)) # Не допускает случайной сборки default revision.
|
||||
vivado = self.run_flow("vivado") # Активная реализация зависит от синтеза с нестандартным именем.
|
||||
self.assertIn(("launch_runs", "synth_board"), vivado) # Собирается именно родитель выбранной реализации.
|
||||
self.assertIn(("update_compile_order", "-fileset", "sources_board"), vivado) # Паспорт относится к набору исходников этого синтеза.
|
||||
self.assertLess(vivado.index(("wait_on_run", "synth_board")), vivado.index(("launch_runs", "impl_board", "-to_step", "write_bitstream"))) # Реализация начинается только после ожидания синтеза.
|
||||
|
||||
|
||||
if __name__ == "__main__": # Поддерживает прямой запуск файла проверки.
|
||||
unittest.main() # Возвращает ненулевой код при нарушении порядка сборки.
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Проверяет FPGA-паспорт, общий итератор и точные байты архивов без лицензии САПР.""" # Запуск через unittest discover из корня репозитория.
|
||||
import importlib.util # Импортирует реальный FPGA-адаптер без установки пакета.
|
||||
import json # Сравнивает конфигурацию, метаданные и manifest готового образа.
|
||||
import os # Управляет меткой времени файла в тесте воспроизводимой пересборки.
|
||||
from pathlib import Path # Ограничивает изменения собственными временными каталогами.
|
||||
import re # Извлекает значения из HDL для проверки общего регистрационного контракта.
|
||||
import tempfile # Изолирует результаты от рабочих проектов пользователя.
|
||||
import unittest # Проверяет успешные сборки и отказы при устаревших файлах.
|
||||
from unittest.mock import patch # Делает Git и время независимыми от машины запуска.
|
||||
TOOL = Path(__file__).resolve().parents[1] / "identity.py" # Находит инструмент рядом с тестами.
|
||||
SPEC = importlib.util.spec_from_file_location("fpga_identity", TOOL) # Создаёт отдельное имя для адаптера.
|
||||
fpga = importlib.util.module_from_spec(SPEC) # Подготавливает импорт без вызова CLI.
|
||||
SPEC.loader.exec_module(fpga) # Загружает код, используемый всеми тремя сценариями САПР.
|
||||
|
||||
|
||||
class IdentityTests(unittest.TestCase): # Проверяет границу между общим генератором и HDL.
|
||||
def setUp(self): # Создаёт проект с кириллицей и пробелами в пути.
|
||||
temporary = tempfile.TemporaryDirectory(prefix="ПЛИС project ") # Проверяет UTF-8 и корректную передачу путей.
|
||||
self.addCleanup(temporary.cleanup) # Очищает только файлы данного теста.
|
||||
self.root = Path(temporary.name) # Хранит все входы и выходы внутри временного проекта.
|
||||
self.config = self.root / "firmware_identity.json" # Служит единственным источником ручного номера версии.
|
||||
self.generated = self.root / "build/generated" # Имитирует каталог текущей конфигурации сборки.
|
||||
self.image = self.root / "board.bit" # Нативный результат ISE или Vivado.
|
||||
self.config.write_text(json.dumps({"product": "Плата", "target": "test-part", "version": [1, 2, 3], "increment_version": True}), encoding="utf-8") # Не изменяет пример или настройки реальных проектов.
|
||||
self.git = patch.object(fpga.shared, "git_state", return_value={"commit": None, "dirty": None}) # Без Git должен получаться явный NOGIT000.
|
||||
self.git.start() # Фиксирует отсутствие Git для всех вызовов данного теста.
|
||||
self.addCleanup(self.git.stop) # Восстанавливает функцию после проверки.
|
||||
self.epoch = patch.dict(os.environ, {"SOURCE_DATE_EPOCH": "1792413296"}) # UTC 2026-10-19 12:34:56 позволяет проверить точные календарные слова.
|
||||
self.epoch.start() # Делает дату HDL детерминированной.
|
||||
self.addCleanup(self.epoch.stop) # Не меняет окружение остальных тестов.
|
||||
|
||||
def prepare(self, toolchain="vivado", language="verilog"): # Вызывает публичную функцию адаптера теми же аргументами, что Tcl.
|
||||
return fpga.prepare(self.root, self.config, self.generated, self.image, toolchain, language) # Делегирует резервирование версии общему prepare.
|
||||
|
||||
def test_hdl_matches_contract_and_iterator(self): # Одним вектором сравнивает обе HDL-реализации с контрактом C.
|
||||
metadata = self.prepare() # Первый запуск должен увеличить patch ровно один раз.
|
||||
expected = [1, 1, 2, 4, 2026, 0x0A13, 0x0C22, 56, 0x4E4F, 0x4749, 0x5430, 0x3030] # 12 слов: контракт, 1.2.4, UTC и ASCII NOGIT000.
|
||||
self.assertEqual(metadata["fpga"]["register_words"], expected) # Фиксирует независимый эталон адресов и старшинства октетов.
|
||||
verilog = (self.generated / "firmware_identity.v").read_text(encoding="utf-8") # Читает фактически записанный файл для синтеза XST.
|
||||
vhdl = (self.generated / "firmware_identity.vhd").read_text(encoding="utf-8") # Читает альтернативный VHDL-93 паспорт.
|
||||
self.assertEqual([int(value, 16) for value in re.findall(r"word_data = 16'h([0-9A-F]{4})", verilog)][:12], expected) # Проверяет константы без зависимости от пробелов и комментариев.
|
||||
self.assertEqual([int(value, 16) for value in re.findall(r'x"([0-9A-F]{4})" when "[01]{4}"', vhdl)], expected) # Проверяет то же расположение слов в VHDL.
|
||||
self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], [1, 2, 4]) # HDL и сохраняемый счётчик обязаны совпадать.
|
||||
self.assertEqual(self.prepare()["version"], [1, 2, 5]) # Следующая попытка использует следующий номер общего итератора.
|
||||
|
||||
def test_seal_archive_export_and_tampering(self): # Проверяет реальную упаковку точных байтов и отказ при повреждении.
|
||||
self.prepare() # Фиксирует паспорт до появления готового .bit.
|
||||
data = b"test-only FPGA bitstream\x00\xff" # Произвольные байты моделируют файл программатора без разбора вендорного контейнера.
|
||||
self.image.write_bytes(data) # Имитирует успешный выход полной сборки САПР.
|
||||
result = fpga.finish(self.generated / "firmware_identity.json", self.root / "firmware", self.root / "export") # Запускает общий seal, verify, archive и проверку экспорта.
|
||||
for folder in (Path(result["directory"]), Path(result["exported_directory"])): # Проверяет обе сохраняемые копии текущего выпуска.
|
||||
self.assertEqual((folder / self.image.name).read_bytes(), data) # Архивы должны содержать точные исходные байты.
|
||||
self.assertEqual((folder / "firmware_identity.v").read_bytes(), (self.generated / "firmware_identity.v").read_bytes()) # Вместе с образом сохраняется точный исходник паспорта, переданный синтезатору.
|
||||
manifest = fpga.shared.verify(folder / self.image.name, folder / (self.image.name + ".manifest.json")) # Повторно считает размер и SHA-256 независимо от результата finish.
|
||||
self.assertEqual(manifest["version"], [1, 2, 4]) # Post-build не увеличивает версию второй раз.
|
||||
self.image.write_bytes(b"damaged") # Имитирует изменение файла после выпуска manifest.
|
||||
with self.assertRaises(ValueError): # Проверка обязана обнаружить повреждение до программирования.
|
||||
fpga.shared.verify(self.image, self.image.with_suffix(".bit.manifest.json")) # Использует публичную общую проверку образа.
|
||||
|
||||
def test_stale_missing_and_empty_images_are_rejected(self): # Отказы сборки не должны связывать новый паспорт со старым результатом.
|
||||
self.image.write_bytes(b"old image") # Оставляет нативный файл от предыдущего выпуска.
|
||||
self.prepare() # Запоминает его содержимое и время до синтеза.
|
||||
with self.assertRaisesRegex(ValueError, "not updated"): # Неизменившийся старый файл не считается новым выпуском.
|
||||
fpga.finish(self.generated / "firmware_identity.json", self.root / "firmware") # Отказывается до создания manifest.
|
||||
self.image.unlink() # Моделирует отсутствие результата после сбоя САПР.
|
||||
with self.assertRaisesRegex(ValueError, "Missing or empty"): # Нет образа — нет архива.
|
||||
fpga.finish(self.generated / "firmware_identity.json", self.root / "firmware") # Проверяет отсутствие файла.
|
||||
self.image.write_bytes(b"") # Моделирует оборванную запись выходного файла.
|
||||
with self.assertRaisesRegex(ValueError, "Missing or empty"): # Пустой файл также блокирует выпуск.
|
||||
fpga.finish(self.generated / "firmware_identity.json", self.root / "firmware") # Не допускает SHA-256 пустого файла как прошивки.
|
||||
self.assertFalse((self.root / "firmware").exists()) # Ни один из отказов не должен создать архив.
|
||||
|
||||
def test_reproducible_image_with_new_mtime_is_accepted(self): # Ручная версия и воспроизводимый BitGen могут дать прежние байты.
|
||||
self.image.write_bytes(b"same image") # Готовит существующий результат предыдущей сборки.
|
||||
metadata = self.prepare() # Запоминает прежнее время записи.
|
||||
stamp = metadata["fpga"]["image_before"]["mtime_ns"] + 2_000_000_000 # Две секунды покрывают даже грубую точность времени старых файловых систем.
|
||||
os.utime(self.image, ns=(stamp, stamp)) # Имитирует реальную повторную запись тех же байтов.
|
||||
fpga.finish(self.generated / "firmware_identity.json", self.root / "firmware") # Обновлённый файл допускается без требования отличающегося хеша.
|
||||
|
||||
def test_quartus_and_wrong_extension(self): # Проверяет нативный .sof и отказы до резервирования номера.
|
||||
original = self.config.read_bytes() # Неверная команда не должна увеличивать счётчик.
|
||||
with self.assertRaises(ValueError): # .bit не является нативным результатом Quartus.
|
||||
self.prepare(toolchain="quartus") # Проверяет соответствие расширения выбранной САПР.
|
||||
self.assertEqual(self.config.read_bytes(), original) # Конфигурация остаётся прежней после отказа.
|
||||
self.image = self.root / "revision.sof" # Выбирает штатный выход Assembler.
|
||||
metadata = self.prepare(toolchain="quartus", language="vhdl") # Использует тот же итератор для Altera и VHDL.
|
||||
self.image.write_bytes(b"test SOF") # Моделирует успешную компиляцию Quartus.
|
||||
result = fpga.finish(self.generated / "firmware_identity.json", self.root / "firmware") # Архивирует .sof через общий механизм.
|
||||
self.assertEqual(result["file"], "revision.sof") # Сохраняет имя ревизии программатора.
|
||||
self.assertEqual(metadata["fpga"]["language"], "vhdl") # Фиксирует язык для последующего аудита.
|
||||
|
||||
|
||||
if __name__ == "__main__": # Позволяет запускать проверку отдельным файлом.
|
||||
unittest.main() # Возвращает ненулевой код при нарушении контракта.
|
||||
@@ -0,0 +1,138 @@
|
||||
"""Проверяет автоматическую установку ISE, сохранение версии и локальные BAT.""" # Тесты не изменяют реальные .xise пользователя.
|
||||
import importlib.util # Загружает установщик без вызова его интерактивного CLI.
|
||||
import json # Проверяет автоматически извлечённые и сохраняемые настройки проекта.
|
||||
import os # Подготавливает окружение Windows для реального запуска BAT.
|
||||
from pathlib import Path # Изолирует проекты в игнорируемом каталоге репозитория.
|
||||
import subprocess # Проверяет установочный BAT и установленную команду --check.
|
||||
import tempfile # Создаёт независимый проект для каждой проверки.
|
||||
import unittest # Проверяет успешную установку, повторные вызовы и отказы до сборки.
|
||||
from unittest.mock import patch # Заменяет только внешние команды ISE в тестах запуска.
|
||||
HERE = Path(__file__).resolve().parents[1] # Находит общий установщик рядом с остальными FPGA-инструментами.
|
||||
SPEC = importlib.util.spec_from_file_location("ise_project_setup", HERE / "ise_project.py") # Выделяет установщику отдельное пространство имён.
|
||||
ise = importlib.util.module_from_spec(SPEC) # Создаёт модуль для проверки публичных функций.
|
||||
SPEC.loader.exec_module(ise) # Загружает код без запросов к пользователю и без установки в чужие каталоги.
|
||||
PROJECT = '''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://www.xilinx.com/XMLSchema" xmlns:x="http://www.xilinx.com/XMLSchema">
|
||||
<properties>
|
||||
<property x:name="Device" x:value="xc6slx16"/>
|
||||
<property x:name="Device Family" x:value="Spartan6"/>
|
||||
<property x:name="Package" x:value="ftg256"/>
|
||||
<property x:name="Speed Grade" x:value="-3"/>
|
||||
<property x:name="Implementation Top" x:value="Module|logic_top"/>
|
||||
<property x:name="Implementation Top File" x:value="logic_top.v"/>
|
||||
<property x:name="Preferred Language" x:value="Verilog"/>
|
||||
<property x:name="Working Directory" x:value="impl"/>
|
||||
<property x:name="Output File Name" x:value="programmer_top"/>
|
||||
</properties>
|
||||
</project>
|
||||
''' # Фикстура воспроизводит namespace и свойства настоящего ISE 14.7 без исходников платы.
|
||||
|
||||
|
||||
class SetupTests(unittest.TestCase): # Проверяет автоматическое подключение одного проекта без ручного копирования.
|
||||
def setUp(self): # Каждый сценарий получает свой рабочий каталог с пробелами и кириллицей.
|
||||
scratch = HERE.parents[2] / ".codex-build" # Держит тестовые проекты внутри разрешённой области и вне индекса Git.
|
||||
scratch.mkdir(exist_ok=True) # Поддерживает запуск тестов из свежей копии библиотеки.
|
||||
temporary = tempfile.TemporaryDirectory(prefix="ISE setup плата & ", dir=scratch) # Амперсанд проверяет передачу путей без вставки shell-команд.
|
||||
self.addCleanup(temporary.cleanup) # Удаляет только собственный тестовый проект.
|
||||
self.root = Path(temporary.name) # Хранит весь созданный комплект рядом с тестовым .xise.
|
||||
self.project = self.root / "board.xise" # Отличается от имени top и результата BitGen для проверки автоподбора.
|
||||
self.project.write_text(PROJECT, encoding="utf-8") # Не изменяет существующие проекты на диске.
|
||||
|
||||
def install(self, **arguments): # Вызывает установщик без поиска реальной САПР на каждом тесте.
|
||||
with patch.object(ise, "find_settings", return_value=""): # Автоматическая установка допускается и до установки ISE.
|
||||
return ise.install(self.project, **arguments) # Проверяет настоящую запись обвязок и предварительного HDL.
|
||||
|
||||
def test_install_detects_project_and_does_not_consume_version(self): # Установка должна завершиться готовым локальным BAT и исходниками паспорта.
|
||||
before = self.project.read_bytes() # Автоматическое подключение не должно менять XML проекта без ISE.
|
||||
settings = self.install() # Извлекает параметры и создаёт локальные файлы.
|
||||
self.assertEqual(settings["image"], "impl/programmer_top.bit") # Output File Name и Working Directory важнее имени .xise.
|
||||
self.assertEqual(settings["language"], "verilog") # Определяет язык по реальному верхнему исходнику.
|
||||
identity = json.loads((self.root / "firmware_identity.json").read_text(encoding="utf-8")) # Читает рабочий, а не временный счётчик.
|
||||
self.assertEqual(identity["target"], "xc6slx16-3ftg256") # Паспорт получает реальный кристалл с корпусом и скоростной группой.
|
||||
self.assertEqual(identity["version"], [0, 1, 0]) # Установка не является попыткой синтеза и не расходует номер.
|
||||
self.assertEqual(self.project.read_bytes(), before) # Исходный .xise сохранён побайтно.
|
||||
for name in ("build_firmware.bat", "firmware_build.py", "firmware_build.json", "FIRMWARE_SETUP.md", "firmware_identity_instance.txt", "build/ise/generated/firmware_identity.v"): # Все ранее ручные файлы создаются автоматически.
|
||||
self.assertTrue((self.root / name).is_file(), name) # Проверяет полноту установленного комплекта.
|
||||
self.assertIn("build/ise/", (self.root / ".gitignore").read_text(encoding="utf-8")) # Сгенерированные исходники исключаются из статуса Git проекта.
|
||||
|
||||
def test_reinstall_preserves_configs_version_and_ignore(self): # Повторное подключение не должно сбросить уже выпущенный номер.
|
||||
self.install() # Сначала создаёт штатный набор локальных файлов.
|
||||
identity = self.root / "firmware_identity.json" # Рабочий счётчик изменяется только как часть тестового сценария.
|
||||
identity.write_text('{"product":"Моя плата","target":"xc6slx16","version":[3,4,5],"increment_version":true}', encoding="utf-8") # Имитирует существующую версию и пользовательское форматирование.
|
||||
config = self.root / "firmware_build.json" # Параметры установки также сохраняются при повторном запуске.
|
||||
settings = json.loads(config.read_text(encoding="utf-8")) # Сохраняет автоматически определённые значения остальных полей.
|
||||
settings["output"] = "build/custom" # Проект может поменять каталог архивов после установки.
|
||||
config.write_text(json.dumps(settings), encoding="utf-8") # Создаёт пользовательский формат настройки.
|
||||
originals = {path: path.read_bytes() for path in (identity, config)} # Проверяет побайтную сохранность обоих JSON.
|
||||
self.install() # Повторный вызов должен использовать выбранный пользователем output.
|
||||
for path, data in originals.items(): # Ни версия, ни пути не должны замениться значениями из .xise.
|
||||
self.assertEqual(path.read_bytes(), data) # Не допускает переформатирования сохранённых настроек.
|
||||
ignore = (self.root / ".gitignore").read_bytes() # Запоминает результат после добавления custom output.
|
||||
self.install() # Третий вызов проверяет отсутствие повторяющихся правил.
|
||||
self.assertEqual((self.root / ".gitignore").read_bytes(), ignore) # .gitignore не растёт при каждом запуске.
|
||||
self.assertTrue((self.root / "build/custom/generated/firmware_identity.v").is_file()) # Предварительный HDL соответствует сохранённой настройке.
|
||||
|
||||
def test_existing_user_bat_is_preserved_without_partial_installation(self): # Установщик не должен заменять существующие проектные команды.
|
||||
custom = self.root / "build_firmware.bat" # Создаёт пользовательскую команду с занятым именем.
|
||||
custom.write_bytes(b"@echo custom build\n") # Содержимое отличается от управляемого шаблона.
|
||||
with self.assertRaisesRegex(ValueError, "Existing file differs"): # Коллизия должна быть обнаружена до записи JSON и generated.
|
||||
self.install() # Выполняет настоящую предварительную проверку устанавливаемых файлов.
|
||||
self.assertEqual(custom.read_bytes(), b"@echo custom build\n") # Пользовательский файл остаётся прежним.
|
||||
self.assertFalse((self.root / "firmware_identity.json").exists()) # Частичная установка не создаёт рабочий счётчик.
|
||||
self.assertFalse((self.root / "build").exists()) # Отказ коллизии не оставляет предварительный паспорт.
|
||||
|
||||
def test_vhdl_override_and_ambiguous_or_cpld_project(self): # Поддерживает VHDL и останавливает неподходящие проекты до записи файлов.
|
||||
self.project.write_text(PROJECT.replace("logic_top.v", "logic_top.vhd"), encoding="utf-8") # Расширение top имеет приоритет над Preferred Language.
|
||||
project, image, language, target = ise.project_info(self.root) # Единственный .xise выбирается из переданной папки.
|
||||
self.assertEqual(language, "vhdl") # Выбирает VHDL автоматически без ручного параметра.
|
||||
settings = self.install(image=Path("custom/result.bit")) # Разрешает явно переопределить нестандартный путь BitGen.
|
||||
self.assertEqual(settings["image"], "custom/result.bit") # Не теряет ручное уточнение при автоматическом заполнении.
|
||||
self.assertIn("component firmware_identity", (self.root / "firmware_identity_instance.txt").read_text(encoding="utf-8")) # Выдаёт VHDL-заготовку для подключения до первого синтеза.
|
||||
(self.root / "other.xise").write_text(PROJECT, encoding="utf-8") # Моделирует две платы в одной папке.
|
||||
with self.assertRaisesRegex(ValueError, "exactly one"): # Установщик не должен угадывать нужную плату.
|
||||
ise.project_info(self.root) # Требует явный файл .xise.
|
||||
self.project.write_text(PROJECT.replace("xc6slx16", "xc9572xl"), encoding="utf-8") # CPLD требует .jed вместо .bit.
|
||||
with self.assertRaisesRegex(ValueError, "FPGA project"): # Неподходящий FPGA-шаблон не устанавливается на CPLD.
|
||||
ise.project_info(self.project) # Проверяет устройство до любых изменений проекта.
|
||||
|
||||
def test_build_preserves_arguments_exit_code_and_check_version(self): # Локальная настройка должна доходить до общего Tcl отдельными аргументами.
|
||||
self.install() # Готовит настоящий набор настроек и исходную версию.
|
||||
config = self.root / "firmware_build.json" # Выбирает установленную локальную настройку.
|
||||
version = (self.root / "firmware_identity.json").read_bytes() # --check не имеет права занимать следующий номер.
|
||||
with patch.object(ise, "ise_environment", return_value={"PATH": "test"}), patch.object(ise.shutil, "which", return_value="C:/ISE path/xtclsh.exe"), patch.object(ise.subprocess, "call", return_value=7) as invoke: # Не запускает лицензируемую САПР в тесте аргументов.
|
||||
self.assertEqual(ise.build(config, check=True), 0) # Проверка должна завершиться до запуска Tcl-flow.
|
||||
invoke.assert_not_called() # Подтверждает отсутствие скрытой сборки в --check.
|
||||
self.assertEqual(ise.build(config), 7) # Код ошибки САПР передаётся пользователю без подмены.
|
||||
arguments = invoke.call_args.args[0] # Каждый элемент списка соответствует одному аргументу процесса.
|
||||
self.assertEqual(arguments[2], str(self.project)) # Пробелы, кириллица и амперсанд сохраняются в пути .xise.
|
||||
self.assertEqual(arguments[-2], "verilog") # Общий Tcl получает выбранный при установке язык.
|
||||
self.assertEqual(invoke.call_args.kwargs["cwd"], self.root) # Относительные исходники .xise разрешаются из папки проекта.
|
||||
self.assertEqual((self.root / "firmware_identity.json").read_bytes(), version) # До настоящего pre-build номер не изменяется.
|
||||
|
||||
@unittest.skipUnless(os.name == "nt", "Windows BAT execution") # Реальные BAT проверяются только на Windows, остальные тесты переносимы.
|
||||
def test_native_settings_environment_preserves_unicode(self): # Автоматическое окружение должно сохранить пути при любой кодовой странице консоли.
|
||||
settings = self.root / "settings64.bat" # Путь содержит пробелы, кириллицу и амперсанд.
|
||||
settings.write_bytes('@echo off\r\n@rem Передаёт переменную с русским путём из исходного окружения.\r\n@set "ISE_TEST_RESULT=%FW_TEST_UNICODE%"\r\n'.encode("utf-8")) # Моделирует штатный settings.bat без настоящей установки ISE.
|
||||
expected = str(self.root / "русская папка") # Результат не должен зависеть от OEM-кодировки Windows.
|
||||
with patch.dict(os.environ, {"FW_TEST_UNICODE": expected}), patch.object(ise.shutil, "which", return_value=None): # Принуждает настройку окружения через settings.bat.
|
||||
environment = ise.ise_environment({"ise_settings": str(settings)}) # Исполняет настоящий CMD /u с захватом SET.
|
||||
self.assertEqual(environment["ISE_TEST_RESULT"], expected) # Декодирование UTF-16LE должно сохранить каждый символ пути.
|
||||
|
||||
@unittest.skipUnless(os.name == "nt", "Windows BAT execution") # Реальный запуск обвязок требует командного процессора Windows.
|
||||
def test_real_installer_and_local_bat_check(self): # Проверяет запуск пользователем без ручного копирования файлов или длинной Tcl-команды.
|
||||
environment = os.environ.copy() # Наследует Python launcher и стандартные системные программы.
|
||||
environment["FW_NO_PAUSE"] = "1" # Автотест не должен ожидать клавиши установщика.
|
||||
setup = subprocess.run([environment.get("COMSPEC", "cmd.exe"), "/d", "/c", "call", str(HERE / "INSTALL_ISE.bat"), str(self.project)], env=environment, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # CALL сохраняет кавычки имени BAT; аргумент проекта содержит пробелы и амперсанд.
|
||||
self.assertEqual(setup.returncode, 0, setup.stderr.decode(errors="replace")) # Файлы должен создать сам установщик.
|
||||
stub = self.root / "stub bin" # Создаёт отдельный PATH для проверки наличия xtclsh.
|
||||
stub.mkdir() # Не подменяет системную установку ISE.
|
||||
(stub / "xtclsh.bat").write_text("@rem Только маркер наличия ISE для --check.\n@exit /b 99\n", encoding="utf-8") # Проверка не должна выполнять эту заглушку.
|
||||
environment["PATH"] = str(stub) + os.pathsep + environment.get("PATH", "") # Добавляет тестовую САПР перед остальными программами.
|
||||
before = (self.root / "firmware_identity.json").read_bytes() # Сохраняет номер перед запуском установленного BAT.
|
||||
run = subprocess.run([environment.get("COMSPEC", "cmd.exe"), "/d", "/c", "call", str(self.root / "build_firmware.bat"), "--check"], cwd=HERE, env=environment, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Рабочий каталог специально отличается от каталога проекта; CALL сохраняет кавычки пути BAT.
|
||||
self.assertEqual(run.returncode, 0, run.stderr.decode(errors="replace")) # Локальная Python-обвязка должна найти настройку и библиотеку самостоятельно.
|
||||
self.assertEqual((self.root / "firmware_identity.json").read_bytes(), before) # Проверка установленного комплекта не увеличивает версию.
|
||||
|
||||
|
||||
if __name__ == "__main__": # Поддерживает прямой запуск тестов установщика.
|
||||
unittest.main() # Возвращает ненулевой код при ошибке установки или потере настроек.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Находит общий код независимо от рабочего каталога Vivado.
|
||||
source [file join [file dirname [info script]] common.tcl]
|
||||
# Объединяет запуск и контроль результата в одну прерываемую при ошибке операцию.
|
||||
if {[catch {
|
||||
# Принимает project.xpr, config.json, output, top.bit, язык и необязательный python.exe.
|
||||
fw::init $argv vivado
|
||||
# Открывает проект с его part, top, XDC и настройками реализации.
|
||||
open_project $fw::settings(project)
|
||||
# Берёт активную реализацию, а не предполагает стандартное имя impl_1.
|
||||
set implementation [current_run -implementation]
|
||||
# Синтез выбирается по родителю реализации, даже если в GUI активен другой synth run.
|
||||
set synthesis [get_property PARENT $implementation]
|
||||
# Файловый набор принадлежит именно синтезу выбранной реализации.
|
||||
set sources [get_property SRCSET [get_runs $synthesis]]
|
||||
# Генерирует паспорт до запуска синтеза; повторное увеличение в hooks проекта не требуется.
|
||||
fw::prepare
|
||||
# Регистрирует только один вариант HDL в правильном наборе исходников.
|
||||
add_files -fileset $sources -norecurse $fw::settings(hdl)
|
||||
# Обновляет порядок компиляции для нового модуля или VHDL entity.
|
||||
update_compile_order -fileset $sources
|
||||
# Сбрасывает реализацию перед синтезом, чтобы не использовать прежнюю размещённую схему.
|
||||
reset_run $implementation
|
||||
# Полный синтез обязателен при новом паспорте даже в ручном режиме номера версии.
|
||||
reset_run $synthesis
|
||||
# Запускает синтез с настройками проекта.
|
||||
launch_runs $synthesis
|
||||
# Ожидает завершения дочернего процесса до проверки результата.
|
||||
wait_on_run $synthesis
|
||||
# Ожидание само по себе не означает успех; 100% проверяется отдельно.
|
||||
if {[get_property PROGRESS [get_runs $synthesis]] ne "100%"} {
|
||||
# Не запускает реализацию при отказе синтеза.
|
||||
error "Vivado synthesis failed"
|
||||
# Заканчивает проверку синтеза.
|
||||
}
|
||||
# Включает write_bitstream, который обычная реализация по умолчанию не выполняет.
|
||||
launch_runs $implementation -to_step write_bitstream
|
||||
# Ждёт записи .bit перед вызовом общего post-build.
|
||||
wait_on_run $implementation
|
||||
# Проверяет завершение полной цепочки генерации файла программирования.
|
||||
if {[get_property PROGRESS $implementation] ne "100%"} {
|
||||
# Отказ маршрутизации, DRC или BitGen блокирует архивирование.
|
||||
error "Vivado implementation/bitstream failed"
|
||||
# Завершает проверку реализации.
|
||||
}
|
||||
# Упаковывает именно ожидаемый .bit после успешной сборки.
|
||||
fw::finish
|
||||
# Завершает работу с сохранёнными настройками проекта.
|
||||
close_project
|
||||
# Ошибка любого шага передаётся внешнему вызывающему процессу.
|
||||
} message]} {
|
||||
# Сохраняет исходное сообщение САПР или общего генератора в журнале.
|
||||
puts stderr $message
|
||||
# Возвращает отказ внешней сборке или CI.
|
||||
exit 1
|
||||
# Заканчивает обработку ошибок.
|
||||
}
|
||||
Reference in New Issue
Block a user