Добавить SCI и J-Link и обновить общие порты устройств

Подключить профили прошивки из JSON и подготовку Python для CCS/Keil.
Добавить SCI-программатор F28335 и поддержку STM32 через J-Link.
Обновить Linux-консоль, CAN485 и Modbus и проверки общих портов.
This commit is contained in:
2026-11-13 17:07:20 +03:00
parent 07929c880a
commit 4d101302be
32 changed files with 1400 additions and 44 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9
View File
@@ -15,6 +15,7 @@ import xml.etree.ElementTree as ET # Проверяет фактические
import sys # Запускает общий CLI и подключает конвертор без зависимости от GUI.
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / 'python')) # Контракт защищённой Flash берётся из того же checkout templates, что общий сборщик.
from protocan.f28335_flash import APP_START, APP_DATA, APP_STOP, coff_sections, validate_words # Проверяет отсутствие сектора A и описание FAB1 одинаково для COFF и SCI8.
from set_devices.firmware_flashing import validate_flashing # Сверяет параметры CAN/RS тем же контрактом, что prebuild Keil/CCS и SETGUI.
def numeric_define(header, name): # Читает единственное числовое C-определение без исполнения выражений из заголовка платы.
text = header.read_text(encoding="utf-8-sig") # Поддерживает UTF-8 с BOM для пользовательских заголовков конфигурации.
@@ -363,9 +364,17 @@ def publish(project, templates, tools_templates, compiler, build_dir, artifact,
metadata = json.loads(metadata_path.read_text(encoding="utf-8")) # Отсутствующий prebuild останавливает работу до создания выпуска.
profile = read_profile(project, templates, eeprom) # Повторно читает текущий выбор EEPROM и общий резерв параметров.
settings = json.loads((project / "firmware_identity.json").read_text(encoding="utf-8-sig")) if (project / "firmware_identity.json").is_file() else metadata # Сверяет product и target с действующим проектом.
if validate_flashing(settings.get("flashing", {})) != metadata.get("flashing", {}): raise ValueError("Flashing settings changed after prebuild; rebuild the project") # Изменённые скорость или адрес после компиляции требуют нового prebuild и согласованного manifest.
expected = {"product": settings["product"], "target": settings["target"], "version": configured_version(project, settings), **({"templates_root": str(templates)} if "eeprom_bytes" in profile else {}), **profile} # Ручной номер и аппаратный вариант обязаны совпадать с prebuild.
if any(metadata.get(key) != value for key, value in expected.items()) or metadata.get("can_boot_profile") != profile.get("can_boot_profile"): # Включение и отключение CAN после prebuild также требуют пересборки, как изменение версии, EEPROM или зависимости.
raise ValueError("Configuration changed: run prebuild and rebuild before publishing") # Требует пересборку вместо подмены старых метаданных.
if metadata.get("flashing", {}).get("default_loader") == "f28335-sci": # Настроенный SCI-профиль автоматически подключает общую сборку приложения и RAM-ядра без проектного Python-файла.
if metadata["target"].upper() != "TMS320F28335": raise ValueError("f28335-sci требует target TMS320F28335") # Не применяет Flash API другого процессора по совпадению транспорта.
from types import SimpleNamespace # Передаёт общие проверки в специализированную ветку без копирования их реализации.
spec = importlib.util.spec_from_file_location("shared_c2000_sci_build", tools_templates / "c/firmware-info/tools/c2000_sci_build.py") # Загружает реализацию именно выбранной полной библиотеки templates.
sci = importlib.util.module_from_spec(spec); spec.loader.exec_module(sci) # Импорт не запускает компилятор или запись аппаратуры.
api = SimpleNamespace(load_identity=load_identity, verify_embedded=verify_embedded, verify_package=verify_package, write_receipt=write_receipt) # Один набор проверок обслуживает прежний и новый маршруты выпуска.
return sci.publish(api, project, compiler, build_dir, artifact, metadata, output, export) # Имя *_flash_rs.bin теперь определяется фактической веткой общего postbuild.
verify_linker_profile(map_path.resolve(), profile) if "eeprom_bytes" in profile else None # Проверяет аппаратный выбор линкера, который нельзя вывести только из Python-конфигурации.
with tempfile.TemporaryDirectory(prefix=".verified-release-", dir=build_dir) as directory: # Единственное место, куда общий postcompile пишет до подтверждения пригодности BIN.
stage = Path(directory) # Изолирует все архивы и промежуточные HEX от постоянных каталогов выпуска.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,13 @@
"""Проверка зависимостей автоматической настройки CCS/Keil до изменения проекта.""" # Запускается общей Python-обвязкой из SETGUI без компиляции или обращения к MCU.
import argparse # Принимает только конкретный профиль загрузки из проверенного JSON.
import sys # Возвращает диагностический код установщику проекта.
from c2000_sci_build import find_c2000ware # Поиск F28335 Kernel A совпадает с настоящим postbuild.
parser = argparse.ArgumentParser(description=__doc__) # Локальная проверка не зависит от запущенной IDE.
parser.add_argument("--loader", default="") # Старые проекты без выбранного SCI-профиля требуют только стандартную библиотеку Python.
args = parser.parse_args() # Не допускает неизвестные параметры проверки окружения.
try: # Отсутствующая зависимость должна остановить применение до создания резервных копий проекта.
if args.loader == "f28335-sci": print("C2000Ware: " + str(find_c2000ware())) # Для родного SCI автоматически проверяет установленное ядро TI.
print("Python: " + sys.executable) # Диагностика показывает фактический интерпретатор prebuild/postbuild.
except (ValueError, OSError) as error: # Ошибки поиска SDK отображаются без Python traceback в интерфейсе оператора.
print(str(error), file=sys.stderr); sys.exit(2) # Ненулевой код не позволяет GUI записать неполную настройку IDE.
+7
View File
@@ -9,6 +9,7 @@ from __future__ import annotations
import argparse
from datetime import datetime, timedelta, timezone
import hashlib
import importlib.util # Загружает общий контракт JSON без импорта протокольного runtime и без зависимости от установленного пакета.
import json
import os
import re
@@ -18,6 +19,10 @@ from pathlib import Path
import subprocess
import tempfile # Резервирует номер сборки через временный файл на том же диске, как общий итератор ветки 166.
_flashing_spec = importlib.util.spec_from_file_location("firmware_flashing_contract", Path(__file__).resolve().parents[3] / "python/set_devices/firmware_flashing.py") # Общий контракт переносится вместе с полным checkout templates.
_flashing = importlib.util.module_from_spec(_flashing_spec) # Изолирует проверку настроек от GUI и системных транспортов.
_flashing_spec.loader.exec_module(_flashing) # Использует ту же проверку полей, что SETGUI при чтении manifest.
def git_state(repository: Path) -> dict:
"""Record full commits, and flag untracked sources as well as tracked edits.
@@ -66,6 +71,7 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
for field in ("product", "target"):
if not isinstance(settings.get(field), str) or not settings[field].strip():
raise ValueError(f"missing {field}")
flashing = _flashing.validate_flashing(settings.get("flashing", {})) # Проверяет скорости и адреса до резервирования следующего номера сборки.
if settings.get("increment_version", False): # Прямые вызовы templates из CCS, Keil и CLI используют один и тот же сохраняемый итератор.
version = settings["version"] = next_version(version) # Вычисляет номер для JSON, заголовка и метаданных только один раз.
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=config.parent, prefix=".firmware-version-", delete=False) as stream: # Временный файл на том же диске позволяет атомарно заменить настройки без частичной записи.
@@ -95,6 +101,7 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
"version": version, "build_id": build_id, "built_at_utc": stamp.isoformat(),
"source": source, "templates": dependency,
"identity_contract": 1, "identity_register_count": 12}
if "flashing" in settings: metadata["flashing"] = flashing # Переносит все CAN/RS-профили в prebuild, manifest, архив и комплект Публикатора без изменения бинарного протокола паспорта.
constants = dict(zip(("FIRMWARE_VERSION_MAJOR", "FIRMWARE_VERSION_MINOR", "FIRMWARE_VERSION_PATCH"), version))
constants.update(dict(zip(("FI_YEAR", "FI_MONTH", "FI_DAY", "FI_HOUR", "FI_MINUTE", "FI_SECOND"),
(stamp.year, stamp.month, stamp.day, stamp.hour, stamp.minute, stamp.second))))
+4
View File
@@ -10,6 +10,7 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
# SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
set(SETPROTOCOL_V2_SOURCES
src/c2000_sci.c # Общий автомат SCI Boot F28335 входит во все host и MCU цели.
src/firmware_image.c
src/altera_logic.c
src/altera_stream.c
@@ -105,6 +106,9 @@ if(SETP_BUILD_TESTS)
target_link_libraries(test_modbus_gateway PRIVATE setprotocol_static) # Проверяет тот же исходник, который выполняется на Zynq.
add_test(NAME modbus_gateway COMMAND test_modbus_gateway) # Делает проверку шлюза частью стандартного CTest.
enable_testing()
add_executable(test_c2000_sci tests/test_c2000_sci.c) # Эталонные байты ROM и TI Kernel A проверяются без устройства.
target_link_libraries(test_c2000_sci PRIVATE setprotocol_static) # Проверяет тот же C99-модуль, что использует ctypes.
add_test(NAME c2000_sci COMMAND test_c2000_sci) # Регистрирует контракт в обычном CTest.
add_executable(test_deif_xdi tests/test_deif_xdi.c) # Прямые проверки C-контракта DEIF.
target_link_libraries(test_deif_xdi PRIVATE setprotocol_static) # Используется единственное общее ядро.
add_test(NAME deif_xdi COMMAND test_deif_xdi "${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/deif-xdi-v1.tsv") # Векторы общие с JNI и Python.
+18
View File
@@ -0,0 +1,18 @@
#ifndef SET_C2000_SCI_H /* Один публичный контракт SCI Boot обслуживает GUI и аппаратные тесты. */
#define SET_C2000_SCI_H /* Защищает объявления от повторного включения. */
#include "pcan_abi.h" /* Получает стабильные типы, size_t и экспорт символов общей библиотеки. */
#ifdef __cplusplus /* C++-потребители используют тот же ABI без изменения имён функций. */
extern "C" { /* Открывает C-связность публичных функций. */
#endif /* Завершает условное начало C-связности. */
PCAN_ABI_API int c2000_sci_decode(const uint8_t *source, size_t size, uint8_t *output, size_t capacity, size_t *written); /* Принимает бинарный SCI8 либо ASCII SCI8 TI со STX/ETX; не принимает Intel HEX. */
PCAN_ABI_API int c2000_sci_validate(const uint8_t *data, size_t size, int flash, uint32_t *entry, uint16_t *sectors); /* Проверяет таблицу, адреса слов C28x, перекрытия и присутствие точки входа до передачи. */
PCAN_ABI_API size_t c2000_sci_session_size(void); /* Возвращает размер хранилища, принадлежащего вызывающему приложению. */
PCAN_ABI_API int c2000_sci_begin(void *storage, size_t size, const uint8_t *kernel, size_t kernel_size, const uint8_t *image, size_t image_size); /* Сохраняет указатели на неизменяемые буферы и проверяет оба образа до autobaud. */
PCAN_ABI_API int c2000_sci_next(void *storage, uint8_t *tx, size_t capacity, size_t *tx_size, size_t *rx_size, uint32_t *delay_ms); /* Возвращает 1 для обмена, 2 для паузы, 0 после терминатора и отрицательное число при ошибке состояния. */
PCAN_ABI_API int c2000_sci_accept(void *storage, const uint8_t *rx, size_t size); /* Сверяет эхо ROM либо checksum TI Kernel A, затем разрешает следующий шаг. */
PCAN_ABI_API unsigned c2000_sci_progress(const void *storage); /* Возвращает завершённую долю передачи 0..100 без доступа к системным часам. */
PCAN_ABI_API size_t c2000_sci_ulitco_enter(uint8_t *output, size_t capacity); /* Создаёт штатную команду Monitor: запись единицы в параметр FLASH_NUM=93. */
#ifdef __cplusplus /* Закрывает C-связность только в C++-единицах трансляции. */
} /* Завершает блок публичного C ABI. */
#endif /* Завершает условную C++-обвязку. */
#endif /* Завершает публичный интерфейс SCI Boot. */
+106
View File
@@ -0,0 +1,106 @@
#include "c2000_sci.h" /* Реализует независимый от ОС SCI Boot и TI Flash Kernel A. */
#include <string.h> /* Копирует уже проверенные байты без платформенных преобразований. */
#define SCI_CHUNK_WORDS 1024U /* Буфер TI f28335_flash_kernel имеет PROG_BUFFER_LENGTH=0x400 слов. */
typedef struct { /* Контекст не владеет образами и не выделяет динамическую память. */
const uint8_t *kernel, *image; /* Сохраняет неизменяемые буферы на весь срок сеанса. */
size_t kernel_size, image_size, kernel_at, image_at, pending_bytes; /* Позиции и длины измеряются физическими октетами файлов. */
unsigned phase, wait, remaining, block_words, extra_zero; /* Отделяет autobaud, байтовое эхо ROM и порционные подтверждения Flash. */
uint16_t expected; /* Хранит ожидаемое эхо либо сумму октетов по контракту TI Kernel A. */
} sci_session_t; /* Размер структуры скрыт за функцией session_size для стабильного ABI. */
static uint16_t word(const uint8_t *p) { return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); } /* SCI8 передаёт каждый 16-битный элемент младшей октетой вперёд. */
static uint32_t address(const uint8_t *p) { return ((uint32_t)word(p) << 16) | word(p + 2); } /* Адрес состоит из старшего слова C28x, затем младшего слова. */
static int white(uint8_t c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; } /* ASCII SCI8 допускает стандартные разделители между октетами. */
static int digit(uint8_t c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'A' && c <= 'F' ? c - 'A' + 10 : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1; } /* Декодирует только шестнадцатеричные цифры TI ASCII. */
int c2000_sci_decode(const uint8_t *source, size_t size, uint8_t *output, size_t capacity, size_t *written) { /* Приводит оба поддержанных формата к одному бинарному буферу SCI8. */
size_t at = 0, count = 0; int high, low; /* Хранит позиции отдельно от результата до полного завершения разбора. */
if (written) *written = 0; /* Ошибка никогда не оставляет длину частично принятого файла. */
if (!source || !output || !written || size < 2) return -1; /* Проверяет указатели и минимальный ключ до чтения данных. */
if (source[0] == 0xAA && source[1] == 0x08) { if (size > capacity) return -2; memmove(output, source, size); *written = size; return 0; } /* Бинарный SCI8 сохраняется побайтно. */
while (at < size && white(source[at])) ++at; /* Пропускает пустые строки перед STX. */
if (at == size || source[at++] != 2) return -3; /* ASCII TI начинается управляющим STX, а не Intel HEX-двоеточием. */
for (;;) { /* Принимает только завершённый ETX поток двухзначных октетов. */
while (at < size && white(source[at])) ++at; /* Разделители не входят в передаваемые байты. */
if (at < size && source[at] == 3) { ++at; break; } /* ETX завершает ASCII-контейнер, не передаваясь MCU. */
if (at + 2 > size || (high = digit(source[at])) < 0 || (low = digit(source[at + 1])) < 0) return -3; /* Усечённая либо нечисловая пара останавливает разбор. */
if (count == capacity) return -2; /* Буфер вызывающего не может быть переполнен большим файлом. */
output[count++] = (uint8_t)((high << 4) | low); at += 2; /* Записывает один проверенный октет. */
if (at < size && !white(source[at]) && source[at] != 3) return -3; /* Склеенные пары и слова шире октета не допускаются. */
} /* После ETX разрешены только пустые разделители. */
while (at < size && white(source[at])) ++at; /* Не принимает скрытые данные после контейнера. */
if (at != size || count < 2 || output[0] != 0xAA || output[1] != 0x08) return -3; /* Проверяет конец ASCII и ключ SCI8 до публикации длины. */
*written = count; return 0; /* Публикует только полностью разобранный бинарный результат. */
} /* Завершает преобразование представления SCI8. */
int c2000_sci_validate(const uint8_t *data, size_t size, int flash, uint32_t *entry, uint16_t *sectors) { /* Проверяет адреса назначения без обращения к устройству. */
size_t at = 22, old_at, payload; uint32_t start, stop, old_start, old_stop, target; uint16_t count, mask = 0; unsigned blocks = 0, have_entry = 0; /* SCI8 имеет 22-октетный заголовок и размеры блоков в словах. */
if (!data || size < 24 || size % 2 || data[0] != 0xAA || data[1] != 0x08) return -4; /* Ключ 0x08AA и чётный размер обязательны для C28x SCI8. */
target = address(data + 18); /* Поле entry находится после ключа и восьми резервных слов ROM. */
while (at + 2 <= size) { /* Каждый блок начинается полным 16-битным размером. */
count = word(data + at); at += 2; /* Нулевой размер обозначает конец адресной таблицы. */
if (!count) { if (at != size || !blocks || !have_entry) return -5; if (entry) *entry = target; if (sectors) *sectors = mask; return 0; } /* Успех требует точного конца и реально загруженной точки входа. */
payload = (size_t)count * 2; /* Один адресуемый элемент C28x занимает два октета файла. */
if (size - at < 4 || payload > size - at - 4 || ++blocks > 256) return -5; /* Ограничивает полную секцию и число диапазонов до обращения к данным. */
start = address(data + at); if (start > UINT32_MAX - count) return -6; stop = start + count; /* Проверяет переполнение словного диапазона. */
if (flash) { /* Приложение SCI пишет внутреннюю Flash F28335, включая обычный аппаратный codestart. */
if (start < 0x300000UL || stop > 0x33FFF8UL || (start < 0x33FFF6UL && stop > 0x33FF80UL)) return -6; /* Запрещает RAM, OTP, CSM reserved и пароли; разрешает только два слова codestart 33FFF6–33FFF7. */
{ uint32_t sector; for (sector = (0x33FFFFUL - (stop - 1)) / 0x8000UL; sector <= (0x33FFFFUL - start) / 0x8000UL; ++sector) mask = (uint16_t)(mask | (1U << sector)); } /* Включает каждый пересечённый сектор по 0x8000 слов; TI нумерует A младшим битом. */
} else if (!((start < 0x800UL && stop <= 0x800UL) || (start >= 0x8000UL && stop <= 0x10000UL))) return -6; /* RAM-ядро ограничено M0/M1 и L0–L7, не затрагивая периферийные регистры. */
old_at = 22; /* Повторно обходит предыдущие заголовки без heap и без таблицы платформенных структур. */
while (old_at + 2 < at) { /* Все предшествующие секции уже имеют проверенные размеры. */
uint16_t old_count = word(data + old_at); old_at += 2; /* Получает длину очередного ранее принятого блока. */
if (old_at == at) break; old_start = address(data + old_at); old_stop = old_start + old_count; /* Текущая секция не сравнивается сама с собой. */
if (start < old_stop && old_start < stop) return -7; old_at += 4 + (size_t)old_count * 2; /* Любое наложение запрещается независимо от совпадения байтов. */
} /* Завершает проверку непересечения нового блока. */
if (start <= target && target < stop) have_entry = 1; at += 4 + payload; /* Учитывает entry только внутри реально присутствующего кода. */
} /* Отсутствующий терминатор не допускается даже при правильных секциях. */
return -5; /* Усечённая таблица запрещена до autobaud и стирания MCU. */
} /* Завершает проверку бинарного SCI8. */
size_t c2000_sci_session_size(void) { return sizeof(sci_session_t); } /* Вызывающий выделяет выровненный буфер достаточного размера. */
int c2000_sci_begin(void *storage, size_t size, const uint8_t *kernel, size_t kernel_size, const uint8_t *image, size_t image_size) { /* Начинает сеанс только после проверки обоих буферов. */
sci_session_t *s = (sci_session_t *)storage; int status; /* Контекст размещён в памяти вызывающего порта. */
if (!s || size < sizeof(*s)) return -1; memset(s, 0, sizeof(*s)); /* Не оставляет состояние предыдущей передачи. */
status = c2000_sci_validate(kernel, kernel_size, 0, NULL, NULL); if (status) return status; /* В RAM можно передать только корректное ядро. */
status = c2000_sci_validate(image, image_size, 1, NULL, NULL); if (status) return status; /* Ошибочный Flash-образ не должен даже запускать загрузку ядра. */
s->kernel = kernel; s->kernel_size = kernel_size; s->image = image; s->image_size = image_size; return 0; /* Срок жизни буферов остаётся обязанностью порта. */
} /* Завершает инициализацию сеанса. */
int c2000_sci_next(void *storage, uint8_t *tx, size_t capacity, size_t *tx_size, size_t *rx_size, uint32_t *delay_ms) { /* Планирует ровно один обмен либо обязательную паузу TI. */
sci_session_t *s = (sci_session_t *)storage; size_t count, i; /* Длины считаются октетами, кроме remaining в словах Flash. */
if (!s || !tx || !tx_size || !rx_size || !delay_ms || s->wait || !s->kernel) return -8; *tx_size = *rx_size = 0; *delay_ms = 0; /* Запрещает новый пакет до проверки ответа на предыдущий. */
if (s->phase == 9) return 0; /* Нулевой терминатор уже передан; аппаратная проверка чтением не заявляется. */
if (s->phase == 2 || s->phase == 4) { ++s->phase; *delay_ms = 3000; return 2; } /* TI f05_DownloadImage выдерживает три секунды после ядра и после его autobaud. */
if (!capacity) return -2; /* Даже одиночный символ синхронизации должен помещаться в буфер. */
if (s->phase == 0 || s->phase == 3) { tx[0] = 'A'; *tx_size = *rx_size = 1; s->expected = 'A'; } /* ROM и Kernel A независимо определяют скорость по эху ASCII A. */
else if (s->phase == 1) { tx[0] = s->kernel[s->kernel_at]; *tx_size = *rx_size = 1; s->expected = tx[0]; } /* ROM подтверждает каждый октет ядра своим точным эхом. */
else if (s->phase == 7) { *rx_size = 2; s->expected = 0; } /* TI Kernel A дополнительно возвращает нулевую сумму для пустого остатка блока, кратного 1024 словам. */
else { /* Прикладной поток подтверждается суммой порций после успешного вызова Flash API. */
if (s->phase == 5) { count = 22; s->image_at = 0; } /* Заголовок подтверждается после стирания выбранных ядром секторов. */
else { /* Обрабатывает полный SCI8-блок без передачи следующего до подтверждения текущего. */
if (!s->remaining) { s->block_words = s->remaining = word(s->image + s->image_at); s->extra_zero = s->block_words > SCI_CHUNK_WORDS && s->block_words % SCI_CHUNK_WORDS == 0; } /* Учитывает точный размер порции TI и его особый пустой остаток. */
if (!s->remaining) { if (capacity < 2) return -2; memcpy(tx, s->image + s->image_at, 2); *tx_size = 2; s->image_at += 2; s->phase = 9; return 1; } /* Терминатор завершает программирование и передаёт управление entry приложения. */
count = (size_t)(s->remaining > SCI_CHUNK_WORDS ? SCI_CHUNK_WORDS : s->remaining) * 2; /* Полезная часть порции не превышает RAM-буфер TI. */
if (s->remaining == s->block_words) count += 6; /* Первая порция включает размер секции и 32-битный словный адрес. */
} /* Получена длина заголовка либо одной порции приложения. */
if (capacity < count) return -2; memcpy(tx, s->image + s->image_at, count); *tx_size = count; *rx_size = 2; s->expected = 0; /* Копирует готовые байты без повторной сборки таблицы в порту GUI. */
for (i = 0; i < count; ++i) s->expected = (uint16_t)(s->expected + tx[i]); s->pending_bytes = count; /* Сумма октетов по модулю 65536 совпадает с TI SendCheckSum. */
} /* Ответ на запланированный обмен должен быть принят до следующего шага. */
s->wait = (unsigned)*rx_size; return 1; /* Нулевой rx_size означает отправку терминатора без недостоверного ожидания ACK. */
} /* Завершает формирование следующего действия. */
int c2000_sci_accept(void *storage, const uint8_t *rx, size_t size) { /* Проверяет фактические ответы ROM и Flash Kernel A. */
sci_session_t *s = (sci_session_t *)storage; unsigned words_sent; /* Состояние алгоритма меняется только после достоверного ответа. */
if (!s || !rx || !s->wait || size != s->wait) return -8; /* Не принимает пустое, лишнее или усечённое подтверждение. */
if ((size == 1 ? rx[0] : word(rx)) != s->expected) return -9; s->wait = 0; /* Несовпадение эха или checksum останавливает передачу. */
if (s->phase == 0) s->phase = 1; /* После первого autobaud начинается байтовая передача RAM-ядра. */
else if (s->phase == 1) { if (++s->kernel_at == s->kernel_size) s->phase = 2; } /* Последний подтверждённый октет ядра запускает паузу его старта. */
else if (s->phase == 3) s->phase = 4; /* После autobaud ядра выдерживается пауза перед стиранием Flash. */
else if (s->phase == 5) { s->image_at = 22; s->phase = 6; } /* Подтверждённый заголовок означает завершившийся вызов erase в ядре. */
else if (s->phase == 7) s->phase = 6; /* Убирает дополнительный ACK пустого остатка до следующей секции. */
else { /* Порция приложения подтверждена после успешного Flash_Program. */
words_sent = (unsigned)(s->pending_bytes - (s->remaining == s->block_words ? 6 : 0)) / 2; /* Отделяет служебный заголовок секции от записанных слов. */
s->remaining -= words_sent; s->image_at += s->pending_bytes; if (!s->remaining && s->extra_zero) s->phase = 7; /* Продвигает поток только на подтверждённое число октетов. */
} /* Завершает успешный переход автомата. */
return 0; /* Порт может запросить следующее действие сеанса. */
} /* Завершает обработку подтверждения. */
unsigned c2000_sci_progress(const void *storage) { const sci_session_t *s = (const sci_session_t *)storage; return !s || !s->image_size ? 0 : s->phase == 9 ? 100 : (unsigned)((s->kernel_at * 20) / s->kernel_size + (s->image_at * 80) / s->image_size); } /* Передача ядра занимает 20 процентов, приложение — остальные 80 процентов шкалы. */
size_t c2000_sci_ulitco_enter(uint8_t *output, size_t capacity) { /* Использует только подтверждённую команду Monitor UlitCo из talk_with_monitor.c и param.c. */
static const uint8_t request[10] = {247, 213, 255, 93, 0, 0, 1, 0, 93, 1}; /* Заголовок F7/D5, MONITOR_WRITE=255, FLASH_NUM=93, значение 1; контрольная сумма 255+93+1=349 (0x015D) передаётся младшим октетом вперёд и проверяется talk_with_monitor.c. */
if (!output || capacity < sizeof(request)) return 0; memcpy(output, request, sizeof(request)); return sizeof(request); /* Возвращает полный штатный десятиоктетный запрос без отправки на устройство. */
} /* Завершает общий кодек перехода UlitCo в первичный загрузчик. */
+37
View File
@@ -0,0 +1,37 @@
#include "c2000_sci.h" /* Проверяет публичный ABI штатного SCI-загрузчика без COM и аппаратного стирания. */
#include <stdlib.h> /* malloc предоставляет корректное выравнивание непрозрачного C-контекста. */
#include <stdio.h> /* Печатает место нарушенного контракта при запуске CTest. */
#include <string.h> /* Сравнивает пакеты с неизменяемыми эталонными октетами. */
#define CHECK(condition) do { if (!(condition)) { fprintf(stderr, "SCI test line %d\n", __LINE__); exit(1); } } while (0) /* Проверки работают также в Release с NDEBUG. */
static const uint8_t ram[] = {0xAA,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x90,2,0,0,0,0,0x90,0x12,0x34,0x56,0x78,0,0}; /* Два слова RAM L4 по адресу 0x9000 и та же точка входа. */
static const uint8_t flash[] = {0xAA,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x32,0,0,0x80,2,0,0x32,0,0,0x80,0x12,0x34,0x56,0x78,0,0}; /* Два слова Flash сектора C по адресу 0x328000; ожидаемая маска TI равна 4. */
static void packet(void *session, const uint8_t *expected, size_t length, size_t reply_size, const uint8_t *reply) { /* Проверяет одну эталонную передачу, не собирая пакет повторным кодеком. */
uint8_t tx[2054]; size_t sent, received; uint32_t delay; /* Максимальная порция TI: 1024 слова и заголовок секции из шести октетов. */
CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 1); /* Следующее действие обязано быть обменом, а не пропущенной паузой. */
CHECK(sent == length && received == reply_size && delay == 0 && !memcmp(tx, expected, length)); /* Проверяет точное содержимое, границы и ожидаемый тип ответа. */
if (reply_size) CHECK(c2000_sci_accept(session, reply, reply_size) == 0); /* Только достоверный ответ разрешает последующее действие. */
} /* Завершает проверку одного шага. */
static void pause_ti(void *session) { /* Ожидание проверяется детерминированно без системных часов и реальной трёхсекундной задержки. */
uint8_t tx[1]; size_t sent, received; uint32_t delay; /* Пауза не требует передачи или ответа. */
CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 2 && !sent && !received && delay == 3000); /* Длительность повторяет официальный host TI для Kernel A. */
} /* Завершает проверку обязательной паузы. */
int main(void) { /* Выполняет эталонный сеанс и основные ошибочные случаи на общем C-ядре. */
void *session = malloc(c2000_sci_session_size()); uint8_t changed[sizeof(flash)], tx[2054], decoded[34]; size_t sent, received, written, i; uint32_t delay, entry; uint16_t sectors; /* Хранилище и выходные значения принадлежат host-тесту. */
static const uint8_t sync[] = {'A'}, header_ack[] = {0x64,1}, block_ack[] = {0xC8,1}, monitor[] = {247,213,255,93,0,0,1,0,93,1}; /* Суммы эталона: заголовок 0x0164, секция 0x01C8; Monitor пишет параметр 93 значением 1 и передаёт сумму 255+93+1=349 (0x015D) младшим октетом вперёд. */
static const uint8_t ascii[] = "\002 AA 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 90 02 00 00 00 00 90 12 34 56 78 00 00 \003\r\n"; /* Независимый ASCII-вектор TI содержит STX/ETX, а управляющие октеты не передаются MCU. */
CHECK(session != NULL); CHECK(c2000_sci_validate(ram, sizeof(ram), 0, &entry, &sectors) == 0 && entry == 0x9000 && sectors == 0); /* RAM-ядро должно иметь реально загруженный entry и не включать Flash-секторы. */
CHECK(c2000_sci_validate(flash, sizeof(flash), 1, &entry, &sectors) == 0 && entry == 0x328000 && sectors == 4); /* Прикладной эталон относится именно к сектору C F28335. */
CHECK(c2000_sci_decode(ascii, sizeof(ascii) - 1, decoded, sizeof(decoded), &written) == 0 && written == sizeof(ram) && !memcmp(decoded, ram, written)); /* Проверяет точное совпадение бинарного и ASCII-представления одного образа. */
CHECK(c2000_sci_begin(session, c2000_sci_session_size(), ram, sizeof(ram), flash, sizeof(flash)) == 0); /* Исправные буферы допускают начало autobaud. */
packet(session, sync, 1, 1, sync); /* ROM подтверждает autobaud одним ASCII A. */
for (i = 0; i < sizeof(ram); ++i) packet(session, ram + i, 1, 1, ram + i); /* Первичный загрузчик обязан подтвердить каждый октет RAM-ядра точным эхом. */
pause_ti(session); packet(session, sync, 1, 1, sync); pause_ti(session); /* Ядро запускается и независимо синхронизирует UART перед erase. */
packet(session, flash, 22, 2, header_ack); packet(session, flash + 22, 10, 2, block_ack); packet(session, flash + 32, 2, 0, NULL); /* Проверяет контрольные суммы после erase и программирования, затем терминатор без ACK. */
CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 0 && c2000_sci_progress(session) == 100); /* Только полный подтверждённый сеанс заканчивает прогрессом 100 процентов. */
memcpy(changed, flash, sizeof(flash)); changed[18] = changed[24] = 0x33; changed[20] = changed[26] = 0x80; changed[21] = changed[27] = 0xFF; CHECK(c2000_sci_validate(changed, sizeof(changed), 1, NULL, NULL) == -6); /* Загруженный entry не обходит запрет записи в CSM-область 0x33FF80. */
CHECK(c2000_sci_validate(flash, sizeof(flash) - 2, 1, NULL, NULL) < 0 && c2000_sci_validate(flash, sizeof(flash), 0, NULL, NULL) < 0); /* Усечение таблицы и подмена RAM-ядра Flash-приложением отклоняются. */
CHECK(c2000_sci_begin(session, c2000_sci_session_size(), ram, sizeof(ram), flash, sizeof(flash)) == 0); CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 1); /* Повторный независимый сеанс нужен для проверки неверного эха. */
tx[0] = 'B'; CHECK(c2000_sci_accept(session, tx, 1) == -9 && c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == -8); /* После ошибки подтверждения автомат не должен продолжать стирание или запись. */
CHECK(c2000_sci_ulitco_enter(tx, sizeof(tx)) == sizeof(monitor) && !memcmp(tx, monitor, sizeof(monitor))); /* Штатный переход Monitor также фиксируется эталонным wire-вектором. */
free(session); return 0; /* Освобождает host-контекст; аппаратный интерфейс нигде не создавался. */
} /* Завершает автономную проверку SCI Boot F28335. */
+1
View File
@@ -24,6 +24,7 @@ INCLUDE = ROOT / "include"
JNI_INCLUDES: list[Path] = []
SOURCES = [
ROOT / "src" / name for name in (
"c2000_sci.c", # Прямая MSVC/CC-сборка включает общий автомат SCI F28335.
"set_modbus_gateway.c", # Экспортирует общий USB RTU/TCP кодек также при прямой MSVC/CC сборке без CMake.
"deif_xdi.c", # Общий DEIF входит в host-библиотеку.
"set_invertor_gas.c", # Собирает общий адаптер двух GAS для STM Invertor при прямом запуске host-builder.
+55
View File
@@ -0,0 +1,55 @@
# Прошивка STM32 через J-Link
В SETGUI откройте **Сервис → Прошивка** и выберите **STM32 / J-Link JTAG или SWD**.
Установленный SEGGER J-Link Commander обнаруживается в Keil, каталоге SEGGER или PATH.
При необходимости укажите `JLink.exe` кнопкой «Обзор». Одноимённый инструмент Java JDK программатором не является.
Выберите микроконтроллер из базы J-Link, JTAG либо SWD, частоту в кГц и фактический объём Flash в КиБ.
Начальная частота — 1000 кГц. При нескольких подключённых J-Link укажите серийный номер нужного программатора.
COM-порт и доступ к BOOT0 для этого режима не требуются.
Выберите HEX или BIN и нажмите «Начать прошивку». HEX сохраняет собственные абсолютные адреса и разрывы;
для BIN укажите адрес в поле «Адрес приложения». При изменении файла после выбора требуется выбрать его заново.
Если есть manifest выпуска, его target должен соответствовать выбранному MCU.
Каждая операция создаёт отдельный каталог в выбранной папке резервных копий:
- `before.bin` — вся Flash до записи;
- `image.hex` — проверенный разреженный снимок записываемых данных;
- `after.bin` — вся Flash после записи;
- `program.jlink`, `program.log`, `program-dll.log` — команды и результаты программирования;
- `run.jlink`, `run.log`, `run-dll.log` — отдельный этап сброса и запуска.
Программирование выполняется через `loadfile` без команды полного стирания.
После встроенной проверки J-Link выполняется независимое побайтное сравнение readback с планом.
Только после успешного сравнения отдельный процесс делает reset и `g`.
Нулевой код выхода без полных снимков Flash не считается успешной записью.
Отмена останавливает собственный процесс Commander; при прерванной записи нужна повторная прошивка.
## UKSVEP STM32F103RC
Для текущей платы оставьте включённым профиль UKSVEP, устройство `STM32F103RC`, объём 256 КиБ.
Приложение начинается с `0x08008000`. Страница параметров `0x0801F800..0x0801FFFF` запрещена для записи,
её сохранность проверяется сравнением `before.bin` и `after.bin`.
Для первой установки выберите разреженный `uksvep_install.hex`, содержащий boot и приложение.
Для обновления через JTAG можно выбрать обычный HEX приложения или BIN с адресом `0x08008000`.
Общий план добавит manifest CRC32 по адресу `0x0803F800`, который проверяет постоянный загрузчик.
Готовый manifest установочного HEX сверяется с приложением. Перед запуском снимаются старые одноразовые
маркеры boot в BKP DR9/DR10 без стирания резервного домена.
Для других плат снимите флажок UKSVEP и задайте их микроконтроллер и объём Flash.
Защита страницы и формирование manifest UKSVEP относятся только к этому явно выбранному профилю.
## Расположение общей реализации
- `python/set_devices/stm32_jtag.py` — неизменяемые настройки, проверка образа, разреженный HEX, сценарии и readback;
- `python/set_devices/qt_ports/stm32_jtag.py` — асинхронный порт QProcess, журналы, тайм-аут, отмена и reset/run;
- `python/set_devices/qt_ports/stm32_jtag_panel.py` — встраиваемая форма Qt 5/6 с сохранением настроек;
- `python/tests/test_stm32_jtag.py` — проверки плана, защиты параметров и ошибок без физического MCU.
Модули не импортируют `gui_desktop`. SETGUI подключает панель к существующим кнопкам, прогрессу и терминалу.
Код загрузчика `stm32-jtag`, транспорт `stm32`, соответствует индексу 8 формы;
профиль `flashing.profiles.stm32-jtag` может задавать `base_address`.
Параметры конкретного J-Link сохраняются локально и не публикуются вместе с прошивкой.
SEGGER Commander должен быть установлен отдельно; пакет SETGUI не скачивает и не включает сторонний executable.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7
View File
@@ -44,3 +44,10 @@ CANDLE_G474_BOOT = { # Профиль воспроизводит CAN_BOOTLOADER
"device_type": 7, "device": 15, "product": 0, "hardware": 0, # Идентичность ProtoCAN BOOT: Type 7, Device 15, Product 0 и аппаратная ревизия 0.
"can_bitrate": 500_000, "uart_baud": 512_000, # Classic CAN работает на 500 кбит/с, USART1 PA9/PA10 — на 512000 бод, 8N1.
} # Адреса и скорости относятся только к CANdle G474VET6 и не подменяют профиль USB BOOT G474RB.
UKSVEP_F103RC_BOOT = { # Профиль отдельного boot-проекта UKSVEP_ICE_22220.6 согласован с ports/stm32f103rc-uksvep/boot_profile.h.
"app_base": 0x08008000, "app_size": 94 * 1024, # Начальные 32 КиБ заняты BOOT; приложение заканчивается перед существующей EEPROM-страницей 0x0801F800.
"stack_base": 0x20000000, "stack_end": 0x2000C000, # DS5792 определяет для STM32F103RC 48 КиБ SRAM и восьмибайтовое выравнивание начального MSP.
"device_type": 5, "product": 0x5606, "hardware": 1, # DeviceID задают активные нулём перемычки PA6/PA7/PC4/PC5; Product отличает ВЭП от других STM32.
"can_bitrate": 250_000, "uart_baud": 115_200, # Загрузчик UKSVEP использует CAN PA11/PA12 и USART1 PA9 TX/PA10 RX на 115200 бод.
} # Single-slot обновление сохраняет параметры; для запуска требуется постоянный manifest и CRC образа.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,135 @@
"""Встроенная прошивка F28335 через штатный SCI Boot без внешнего C2Prog.""" # Qt отвечает за COM и потоки; протокол реализован общим C99-ядром templates.
import hashlib # Проверяет точные байты RAM-ядра, сохранённого в комплекте postbuild.
from pathlib import Path # Разрешает имя RAM-ядра относительно выбранного BIN.
import threading # Передаёт отмену из GUI в рабочий поток без принудительного завершения потока.
import time # Системные часы используются только транспортом для конечных тайм-аутов ожидания.
try: # Поддерживает основные сборки Qt 6 и Windows 7/Qt 5.
from PySide6.QtCore import QThread, Signal, QIODevice # Рабочий поток не блокирует GUI ожиданием UART или Flash API.
from PySide6.QtWidgets import QWidget, QFormLayout, QLineEdit, QPushButton, QCheckBox, QSpinBox, QFileDialog, QLabel # Панель использует обычные контролы SETGUI.
from PySide6.QtSerialPort import QSerialPort # Транспорт создаёт отдельный системный COM-сеанс на время программирования.
except ImportError: # Для Python 3.8/Windows 7 использует те же операции Qt 5.
from PySide2.QtCore import QThread, Signal, QIODevice # Сигналы безопасно возвращают результаты в главный поток.
from PySide2.QtWidgets import QWidget, QFormLayout, QLineEdit, QPushButton, QCheckBox, QSpinBox, QFileDialog, QLabel # Интерфейс не требует компонентов, доступных только Qt 6.
from PySide2.QtSerialPort import QSerialPort # COM-порт остаётся локальной зависимостью адаптера.
from protocan.c2000_sci import Session, decode, validate, ulitco_enter # Сборка, разбор, checksum и автомат обмена не повторяются в порту.
class SciWorker(QThread): # Один сеанс владеет COM-портом и неизменяемыми буферами до завершения.
progress = Signal(int, str) # Штатная полоса GUI получает только подтверждённый прогресс C-автомата.
result = Signal(bool, str) # Ошибка транспорта или checksum не выдаётся за успешную запись.
def __init__(self, kernel, image, port, baud, enter=False, timeout_ms=3000, parent=None): # Все параметры снимаются с GUI до запуска потока.
super().__init__(parent) # Время жизни потока связано с существующей панелью прошивки.
self.kernel, self.image, self.port, self.baud = kernel, image, port, baud # Буферы не перечитываются с диска после аппаратного старта.
self.enter, self.timeout_ms, self.stop = enter, timeout_ms, threading.Event() # Отмена проверяется во всех конечных ожиданиях.
def cancel(self): # GUI не вызывает методы QSerialPort чужого потока.
self.stop.set() # Рабочий поток сам завершит чтение и закроет COM-порт.
def _cancelled(self): # Единая проверка отмены используется в задержках, чтении и записи.
if self.stop.is_set(): raise ValueError("Прошивка остановлена оператором") # Отмена не запускает повторную передачу или попытку erase.
def _pause(self, milliseconds): # Обязательные паузы TI остаются прерываемыми оператором.
end = time.monotonic() + milliseconds / 1000 # Миллисекунды C-автомата преобразуются только в системное ожидание порта.
while time.monotonic() < end: self._cancelled(); self.msleep(min(50, max(1, int((end - time.monotonic()) * 1000)))) # Проверяет отмену не реже одного раза за 50 мс.
def _write(self, serial, data): # Передаёт готовые байты и дожидается фактической отправки UART.
self._cancelled() # Не добавляет новые данные после запроса остановки.
if not data: return # Отдельное подтверждение пустого остатка TI не требует передачи.
if serial.write(data) != len(data): raise OSError("Не удалось поставить полный пакет SCI в очередь COM") # Частичная запись не должна сдвинуть автомат протокола.
end = time.monotonic() + max(5, len(data) * 12 / self.baud + 2) # Бюджет включает время передачи октетов UART и запас драйвера.
while serial.bytesToWrite(): # Очередь обязана опустеть до начала ожидания ответа.
self._cancelled() # Отмена сохраняет конечность длительной отправки на низкой скорости.
serial.waitForBytesWritten(100) # Короткое ожидание позволяет проверять отмену и ошибку драйвера.
if time.monotonic() > end: raise TimeoutError("Истёк тайм-аут передачи SCI") # Не ожидает бесконечно отключённый преобразователь.
def _read(self, serial, count, timeout): # Собирает ровно требуемое число октетов, не разбирая их поля.
result, end = bytearray(), time.monotonic() + timeout / 1000 # Единственный дедлайн сохраняется при частичных UART-порциях.
while len(result) < count: # Фрагментация USB-UART не меняет смысл ответа C-автомата.
self._cancelled() # Оператор может прекратить ожидание недоступного MCU.
if serial.bytesAvailable(): result.extend(bytes(serial.read(count - len(result)))) # Следующий ответ не поглощается текущим чтением.
else: serial.waitForReadyRead(100) # Ожидание остаётся внутри рабочего потока, без зависания GUI.
if time.monotonic() > end: raise TimeoutError("Загрузчик F28335 не подтвердил обмен SCI") # Отсутствие эха или успешного Flash API не трактуется как продолжение.
return bytes(result) # Проверку эха и checksum выполнит C-ядро.
def run(self): # Qt вызывает метод в отдельном потоке, где создаётся и закрывается COM-порт.
serial = None # Ошибка проверки образа не должна даже открывать аппаратный интерфейс.
try: # Все исключения превращаются в конечный результат GUI.
session = Session(self.kernel, self.image) # Проверяет RAM-ядро и Flash-таблицу до любой управляющей команды.
serial = QSerialPort(); serial.setPortName(self.port); serial.setBaudRate(468750 if self.enter else self.baud) # Штатный Monitor ИБП работает на 468750 бит/с (MONITOR_BRR=9, LSPCLK=37,5 МГц); при ручном входе сразу используется скорость загрузчика из manifest.
serial.setDataBits(QSerialPort.DataBits.Data8); serial.setParity(QSerialPort.Parity.NoParity); serial.setStopBits(QSerialPort.StopBits.OneStop); serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) # TI SCI Boot и первичный загрузчик UlitCo используют 8N1 без аппаратного управления потоком.
if not serial.open(QIODevice.OpenModeFlag.ReadWrite): raise OSError(serial.errorString()) # Занятый терминалом порт не открывается вторым владельцем.
serial.clear() # Старые данные терминала не должны стать ложным эхо autobaud.
if self.enter: # Приложение UlitCo принимает переход в загрузчик лишь в разрешённом остановленном состоянии.
self.progress.emit(0, "Переход UlitCo в первичный загрузчик…") # Показывает отдельный этап до передачи RAM-ядра.
self._write(serial, ulitco_enter()); self._pause(500) # Отправляет проверенную команду Monitor №93 на скорости приложения и даёт штатному SCI-приёмнику 500 мс на подготовку autobaud.
if not serial.setBaudRate(self.baud): raise OSError("Не удалось переключить COM на скорость загрузчика SCI") # Только после выхода из приложения включает скорость RAM-ядра из manifest; отказ запрещает дальнейшую передачу.
serial.clear() # Остатки ответов Monitor не должны стать ложным эхом нового autobaud.
while True: # Порядок обменов полностью определяется общим C-автоматом.
self._cancelled(); state, tx, expected, delay = session.next() # Порт не вычисляет пакетные поля или checksum.
if state == 0: break # Подтверждённые порции и отправленный терминатор завершили поток.
if state == 2: self._pause(delay); serial.clear(); continue # TI требует паузу после RAM-ядра и его autobaud.
self._write(serial, tx) # Отправляет готовый октет ROM либо порцию Flash Kernel A.
if expected: session.accept(self._read(serial, expected, 60000 if expected == 2 else self.timeout_ms)) # Erase/program получают минуту; эхо ROM использует точный тайм-аут из профиля JSON в миллисекундах.
self.progress.emit(session.progress, "Ядро TI / запись Flash…") # Продвижение следует только успешно проверенным ответам.
self.result.emit(True, "Передача завершена; ядро TI подтвердило порции Flash. Проверка чтением памяти отдельно не выполнялась.") # Не заявляет недоступную в Kernel A полную readback-проверку.
except (OSError, ValueError, RuntimeError) as error: self.result.emit(False, str(error)) # Ошибка остановит GUI и сохранится в журнале текущей прошивки.
finally: # Владение портом освобождается также после отмены или тайм-аута.
if serial is not None: serial.close() # Закрывает только созданный этим потоком COM-сеанс.
class C2000SciPanel(QWidget): # Общая панель используется SETGUI без зависимости от отдельного приложения C2Prog.
progress = Signal(int, str) # Согласует рабочий поток со штатной полосой прошивки.
finished = Signal(bool, str) # Завершение освобождает форму одинаково после успеха, отказа или отмены.
def __init__(self, parent=None): # Создаёт только параметры маршрута, оставляя COM и скорость основной форме.
super().__init__(parent); self.worker = None; self.metadata = {}; self.path = None # Состояние соответствует одному выбранному Flash BIN.
form = QFormLayout(self) # Контролы не требуют дополнительного окна приложения.
self.kernel = QLineEdit(); form.addRow("RAM-ядро TI", self.kernel) # Готовый комплект заполняет путь автоматически.
button = QPushButton("Выбрать ядро…"); button.clicked.connect(self._choose); form.addRow(button) # Старые образы допускают явный выбор соответствующего ядра.
self.enter_monitor = QCheckBox("Перевести остановленный UlitCo в загрузчик через Monitor №93"); form.addRow(self.enter_monitor) # Управляющая команда отправляется только при явном выбранном режиме.
self.oscillator_hz = QSpinBox(); self.oscillator_hz.setRange(20000000, 30000000); self.oscillator_hz.setSingleStep(5000000); self.oscillator_hz.setValue(30000000); self.oscillator_hz.setReadOnly(True); form.addRow("Кварц ядра, Гц", self.oscillator_hz) # Частота зафиксирована при сборке ядра и не меняется настройкой COM.
self.timeout_ms = QSpinBox(); self.timeout_ms.setRange(50, 10000); self.timeout_ms.setValue(3000); form.addRow("Ожидание связи, мс", self.timeout_ms) # Отдельный erase/program получает обязательный бюджет до 60 секунд.
label = QLabel("Штатный SCI Boot F28335: приложение UlitCo → RAM-ядро TI → Flash. Выберите порт SCI-A; при ручном входе предварительно включите загрузчик."); label.setWordWrap(True); form.addRow(label) # Оператор видит физический интерфейс и последовательность до начала записи.
def _choose(self): # Выбор RAM-ядра сам по себе ничего не отправляет в устройство.
path, _ = QFileDialog.getOpenFileName(self, "RAM-ядро F28335 SCI", self.kernel.text(), "SCI8 (*.bin *.txt)") # Принимает штатные бинарное и ASCII-представления TI.
if path: self.kernel.setText(path) # Отмена сохраняет текущий комплект.
def set_manifest(self, image, metadata): # Подтягивает ядро и параметры из выбранного постсборочного комплекта.
self.path, self.metadata = Path(image), metadata # Сохраняет принадлежность ядра конкретному Flash BIN.
info = metadata.get("flash_kernel", {}); info = info if isinstance(info, dict) else {} # Не относящийся к SCI manifest не должен нарушать визуальную загрузку других режимов.
name = info.get("file", "") # Manifest хранит только исходное имя соседнего RAM-бинарника.
if isinstance(name, str) and name and Path(name).name == name: self.kernel.setText(str(self.path.parent / name)) # Не разрешает metadata-путь за пределы папки образа.
else: self.kernel.clear() # Новый образ не должен наследовать RAM-ядро предыдущей прошивки.
frequency = info.get("oscillator_hz", 30000000); self.oscillator_hz.setValue(frequency if type(frequency) is int and frequency in (20000000, 25000000, 30000000) else 30000000) # Показывает допустимую частоту без исключения setter на чужих metadata.
def prepare(self, image): # Фиксирует и проверяет все байты до подтверждения аппаратной операции.
with Path(image).open("rb") as stream: data = stream.read(1024 * 1024 + 1) # Ограничивает чтение Flash BIN до одного МиБ и одного контрольного октета.
with Path(self.kernel.text()).open("rb") as stream: kernel_raw = stream.read(1024 * 1024 + 1) # Worker получает ограниченный неизменяемый снимок RAM-ядра.
if len(data) > 1024 * 1024 or len(kernel_raw) > 1024 * 1024: raise ValueError("Слишком большой файл SCI8") # Ограничивает отдельные Flash и RAM-образы одним МиБ.
if self.metadata and (len(data) != self.metadata.get("size_bytes") or hashlib.sha256(data).hexdigest() != self.metadata.get("sha256")): raise ValueError("Flash BIN изменён после выбора или не соответствует manifest") # Новый корректный SCI8 не должен незаметно заменить ранее подтверждённый образ.
info = self.metadata.get("flash_kernel", {}) # При наличии паспорта зависимость должна совпадать побайтно.
if not isinstance(info, dict): raise ValueError("В manifest требуется объект flash_kernel") # Неверный тип метаданных возвращается обычной ошибкой комплекта до доступа к COM.
if info and (len(kernel_raw) != info.get("size_bytes") or hashlib.sha256(kernel_raw).hexdigest() != info.get("sha256")): raise ValueError("RAM-ядро TI не соответствует manifest Flash BIN") # Чужая частота или маска erase не должны незаметно попасть в сеанс.
kernel, flash = decode(kernel_raw), decode(data) # Оба представления разбираются только общим C-декодером.
validate(kernel, False); _, sectors = validate(flash, True) # Не открывает COM для повреждённых таблиц или адресов CSM; маску вычисляет единственный C-декодер.
if info and (info.get("sector_mask") != sectors or info.get("oscillator_hz") != self.oscillator_hz.value()): raise ValueError("RAM-ядро собрано для другой частоты или набора секторов Flash") # Комплект обязан соответствовать фактическим адресам приложения и выбранному при сборке кварцу.
return kernel, flash # Возвращает готовые байты для неизменяемой аппаратной операции.
def start(self, job, port, baud): # Основная форма вызывает метод после штатного подтверждения пользователя.
if self.worker is not None: raise ValueError("Прошивка SCI уже выполняется") # Один виджет не может владеть двумя одновременными COM-сеансами.
self.worker = SciWorker(*job, port, baud, self.enter_monitor.isChecked(), self.timeout_ms.value(), self) # Worker получает только снимок параметров главного потока.
self._result = None; self.worker.progress.connect(self.progress.emit); self.worker.result.connect(self._capture_result); self.worker.finished.connect(self._released) # Разблокирует форму только после finally рабочего потока и фактического закрытия COM.
self.worker.start() # До этого места никакие байты MCU не передавались.
def _capture_result(self, success, text): # Промежуточный сигнал не означает, что QThread уже закончил закрытие порта.
self._result = (success, text) # Конечное уведомление будет отправлено после штатного QThread.finished.
def _released(self): # Ссылка на поток сохраняется до завершения его finally и закрытия COM.
worker, self.worker = self.worker, None # После окончания разрешает следующую явную прошивку.
if worker is not None: worker.deleteLater() # Освобождает Qt-объект в главном потоке без уничтожения работающего QThread.
self.finished.emit(*(self._result or (False, "Поток SCI завершился без результата"))) # Форма снова доступна, когда собственный COM-сеанс гарантированно закрыт.
def cancel(self): # Отмена не вызывает terminate и не убивает чужие процессы.
if self.worker is not None: self.worker.cancel() # Рабочий поток завершит своё ближайшее конечное ожидание.
@@ -18,6 +18,7 @@ class Can485Port(QObject): # Совместимая с BoardTab оболочк
self.serial.errorOccurred.connect(self._serial_error); self.tcp.errorOccurred.connect(self._tcp_error); self.tcp.disconnected.connect(self._disconnected) # Потеря соединения останавливает циклическую отправку через обработчик панели.
self.tcp.connected.connect(self._connected) # Начальная настройка выполняется только после подтверждённого TCP handshake.
self.timeout = QTimer(self); self.timeout.setSingleShot(True); self.timeout.timeout.connect(self._timed_out) # Конечный срок TCP не блокирует GUI даже при отключённой плате.
self._shared=None # Единая сборка позволяет CAN и Modbus одновременно использовать один физический FTDI.
def _connected(self): # Завершает фазу ожидания и разрешает запрос версии.
self.timeout.stop(); self.connected.emit() # Запоздалый timeout больше не может закрыть действующую сессию.
@@ -51,7 +52,16 @@ class Can485Port(QObject): # Совместимая с BoardTab оболочк
def connect_tcp(self, host, port): # Endpoint уже проверен UI, DNS и синхронное ожидание не требуются.
self.close(); self.kind = 'Ethernet'; self._endpoint = f'{host}:{port}'; self._error = ''; self.timeout.start(5000); self.tcp.connectToHost(host, port) # Пять секунд ограничивают ожидание недоступного TCP-порта 37579.
def connect_console(self,name,port=37579): # Общий FTDI канал открывает отдельную CAN TCP сессию без захвата COM второй вкладкой.
from .console_link_port import ConsoleLink # Общий менеджер делится с Modbus и сохраняет действующий USB host HDMI.
self.close(); self.kind='USB'; self._endpoint=name; self._error=''; self.console_restored=False; self._shared=ConsoleLink.acquire(name,port,self); channel=self._shared; self.timeout.start(5000) # Начальное открытие остаётся отменяемым до подтверждения backend.
channel.readyRead.connect(self.readyRead); channel.opened.connect(self._connected); channel.errorOccurred.connect(lambda error:self._shared_error(channel,error)) # Прикладные CAN строки и ошибки принадлежат только своему логическому каналу.
def _shared_error(self,channel,error): # Ошибка локального CAN backend не должна выдавать сообщение физического QtSerialPort другого протокола.
if not self._closing: self._error=channel.errorString(); self.errorOccurred.emit(error) # Стандартный BoardTab cleanup завершает только CAN сессию.
def isOpen(self): # Штатные кнопки BoardTab учитывают реальное состояние выбранного канала.
if self._shared is not None: return self._shared.active and self._shared.isOpen() # Общий COM не считается готовым CAN до OPEN конкретного loopback backend.
return self.tcp.state() == QAbstractSocket.SocketState.ConnectedState if self.kind == 'Ethernet' else self.serial.isOpen() and (not self._console or self._console_ready) # Открытый USB tty с незавершённым nc handshake тоже не считается CAN-подключением.
def isConnecting(self): # Позволяет отменить незавершённую попытку подключения повторным нажатием кнопки.
@@ -59,6 +69,7 @@ class Can485Port(QObject): # Совместимая с BoardTab оболочк
def close(self): # Закрытие идемпотентно и не порождает рекурсивную ошибку панели.
self._closing = True; self.timeout.stop(); self.tcp.abort() # Сначала отменяет deadline и сетевые сигналы старой сессии.
if self._shared is not None: channel=self._shared; self._shared=None; channel.close(); self.console_restored=channel.link.console_restored; channel.deleteLater() # Освобождается логический CAN канал, тогда как Modbus продолжает использовать тот же FTDI.
if self._console and self.serial.isOpen(): # Дожидается реального ответа UART, чтобы close не отбросил ещё не переданный Ctrl-C в драйвере Windows.
self.serial.readAll(); self.serial.write(b'\x03'); self.serial.flush() # Удаляет старый поток статуса и немедленно передаёт сигнал завершения собственного foreground nc.
if self.serial.bytesToWrite(): self.serial.waitForBytesWritten(150) # Ограничивает ожидание освобождения локальной выходной очереди.
@@ -70,16 +81,19 @@ class Can485Port(QObject): # Совместимая с BoardTab оболочк
self._console=False; self._console_ready=False; self._console_rx.clear(); self._closing = False # Не оставляет старые данные handshake для следующего COM-порта.
def readAll(self): # Возвращает сырой QByteArray для существующего LineAssembler templates.
if self._shared is not None: return self._shared.readAll() # Общий физический поток уже разделён на независимые CAN/Modbus сессии.
if self._console: data=bytes(self._console_rx); self._console_rx.clear(); return data # Консольные bytes прочитаны единожды при поиске подтверждённого заголовка.
return self.tcp.readAll() if self.kind == 'Ethernet' else self.serial.readAll() # Сегментация TCP/USB не влияет на протокол.
def write(self, data): # Длина результата позволяет обнаружить непринятую целиком команду.
if self._shared is not None: return self._shared.write(data) # Канал кодирует только транспортное обрамление, не меняя исходные CAN команды.
return self.tcp.write(data) if self.kind == 'Ethernet' else self.serial.write(data) # Порт не добавляет заголовков либо переводов строк к готовой команде.
def errorString(self): # Единая диагностика сохраняется до следующего соединения.
return self._error or (self.tcp.errorString() if self.kind == 'Ethernet' else self.serial.errorString()) # Сообщение Qt не подменяется общим «нет связи».
def portName(self): # Нижняя строка монитора показывает фактический канал.
if self._shared is not None: return self._shared.portName() # Общий COM остаётся видимым пользователю при одновременной работе двух протоколов.
return self._endpoint if self.kind == 'Ethernet' else self.serial.portName() # Сетевое соединение отличается от COM-порта платы.
def setPortName(self, name): # Штатный USB-путь BoardTab выбирает физический порт без изменения своего кода.
@@ -0,0 +1,107 @@
"""Общий владелец FTDI и независимые логические TCP-каналы CAN/Modbus поверх Linux shell.""" # Прикладные кодеки не зависят от способа совместного использования COM.
from PySide6.QtCore import QObject, Signal, QTimer, QByteArray, QCoreApplication, QElapsedTimer, QIODevice # Один Qt event loop обслуживает все логические каналы без фоновых гонок.
from PySide6.QtSerialPort import QSerialPort # Физический COM открывается ровно одним экземпляром менеджера.
import shlex # Путь установленного либо явно выбранного тестового helper передаётся shell одним безопасным аргументом.
import re # Подтверждённый root prompt отделяет shell от login/getty и запроса пароля после холодной загрузки.
class ConsoleChannel(QObject): # Контракт readAll/write/close позволяет подключить существующие thin-порты протоколов.
readyRead=Signal(); opened=Signal(); errorOccurred=Signal(object) # OPEN подтверждается loopback службой отдельно от открытия FTDI.
def __init__(self,link,number,port,parent): # Канал принадлежит конкретному потребителю, физическая сессия — приложению.
super().__init__(parent); self.link=link; self.number=number; self.port=port; self.active=False; self.closed=False; self.incoming=bytearray(); self.error='' # Соседние протоколы имеют независимые буферы и состояния.
def isOpen(self): return not self.closed # Ожидающую handshake сессию можно отменить той же кнопкой отключения.
def portName(self): return self.link.name # Панель показывает реальный COM, общий для обеих служб платы.
def errorString(self): return self.error # Ошибка сохраняется до уничтожения конкретной логической сессии.
def readAll(self): data=QByteArray(bytes(self.incoming)); self.incoming.clear(); return data # Каждый прикладной байт читается только своим протоколом.
def write(self,data): # Возвращает длину принятых в очередь данных без подтверждения внешней операции.
raw=bytes(data) # QByteArray и bytes преобразуются без изменения бинарного содержимого.
if self.closed or not self.active: return -1 # Команда не уходит в неподтверждённый или уже закрытый канал.
for offset in range(0,len(raw),512): # Короткая строка HEX позволяет чередовать CAN и Modbus при 115200 бод.
if not self.link.send(f'D{self.number:02X}:'.encode('ascii')+raw[offset:offset+512].hex().encode('ascii')+b'\n'): return -1 # Переполнение физической очереди прекращает сессию без повтора команды.
return len(raw) # Успех означает только локальную постановку в очередь QSerialPort.
def close(self): # Закрытие протокола не отбирает общий COM у оставшихся потребителей.
if self.closed: return # Идемпотентный cleanup безопасен при синхронных Qt error callbacks.
self.closed=True; self.active=False; self.incoming.clear(); self.link.release(self) # Менеджер завершает shell helper только после последнего канала.
def failed(self,message): # Ошибка одного backend не маскируется состоянием другого протокола.
if self.closed: return # Запоздалое событие не возобновляет отключённую страницу.
self.error=message; self.close(); self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # Потребитель очищает собственные незавершённые запросы.
class ConsoleLink(QObject): # Один экземпляр на имя COM обслуживает сколько угодно независимых backend сессий.
links={} # Реестр процесса сохраняет одного физического владельца между вкладками SETGUI.
program='/opt/zynq-link/console_link.py' # Проверка staged-пакета может выбрать его точный локальный путь до постоянной установки helper.
@classmethod # Потребитель запрашивает канал, не управляя временем жизни общей UART-консоли.
def acquire(cls,name,port,parent=None): # Сетевой адрес не передаётся из UI: разрешены только две локальные службы платы.
if int(port) not in (37579,502): raise ValueError('Общий USB поддерживает CAN 37579 и Modbus 502') # Разрешены штатный CAN и стандартный Modbus TCP, без произвольного loopback proxy.
name=str(name).strip(); key=name.upper() if name.upper().startswith('COM') else name # Windows COM нечувствителен к регистру, Linux PTY сохраняет точное имя.
link=cls.links.get(key) # Повторная вкладка присоединяется к уже открытому физическому каналу.
if link is None: link=cls(name,key); cls.links[key]=link; QTimer.singleShot(0,link.start) # Асинхронный запуск даёт потребителю время подключить error/opened callbacks.
number=next((value for value in range(1,256) if value not in link.used),None) # Номер не переиспользуется до закрытия физической сессии, поэтому поздний DATA старого клиента не попадёт новому.
if number is None: raise RuntimeError('Исчерпаны USB-каналы платы') # Ограничение совпадает с однобайтовым номером общего протокола.
channel=ConsoleChannel(link,number,int(port),parent); link.channels[number]=channel; link.used.add(number) # Множество использованных номеров защищает от смешения с поздними queued ответами закрытого backend.
if link.ready: QTimer.singleShot(0,lambda:link.open_channel(channel)) # Существующий helper открывает только новый backend без повторной shell команды.
return channel # Никакая прикладная команда ещё не отправляется в CAN или Modbus.
def __init__(self,name,key): # Общая сессия переживает переключение и закрытие одной рабочей страницы.
super().__init__(QCoreApplication.instance()); self.name=name; self.key=key; self.serial=QSerialPort(self); self.channels={}; self.used=set(); self.incoming=bytearray(); self.ready=False; self.closing=False; self.console_restored=True # QObject приложения освобождает менеджер до завершения Qt runtime, а история номеров изолирует поздние DATA.
self.timer=QTimer(self); self.timer.setSingleShot(True); self.timer.timeout.connect(lambda:self.fail('Нет ответа общего USB-туннеля: проверьте root shell и /opt/zynq-link/console_link.py')) # Ошибка начального handshake имеет конечный срок 5 секунд.
self.serial.readyRead.connect(self.receive); self.serial.errorOccurred.connect(self.serial_error) # Единственный readyRead читает физические байты для всех протоколов.
def start(self): # FTDI открывается только после явного запроса первой страницы подключения.
if self.closing or not self.channels: return # Закрытый до первого event loop запрос не занимает COM.
self.serial.setPortName(self.name); self.serial.setBaudRate(115200); self.serial.setDataBits(QSerialPort.DataBits.Data8); self.serial.setParity(QSerialPort.Parity.NoParity); self.serial.setStopBits(QSerialPort.StopBits.OneStop); self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) # Параметры соответствуют проверенной root-консоли платы, независимо от внешней RS485.
if not self.serial.open(QIODevice.OpenModeFlag.ReadWrite): self.fail(self.serial.errorString()); return # Занятый сторонним терминалом COM не перехватывается.
self.console_restored=False; self.phase='shell'; self.login_sent=False; self.cursor_replied=False; self.timer.start(5000); self.send(b'\x15\r') # Сначала запрашивается фактический prompt: после холодного boot COM может ещё принадлежать getty, а не shell.
def start_helper(self): # Команды tty разрешены только после подтверждения root shell известной платы.
self.phase='helper'; self.incoming.clear(); self.send(("stty -echo -icanon -ixon -ixoff min 1 time 0; python3 "+shlex.quote(self.program)+"; stty sane; printf '\\n#ZYNQ-LINK-CLOSED\\n'\r").encode('ascii')) # Путь экранирован одним аргументом, завершение helper восстанавливает обычный терминал.
def send(self,data): # Общая очередь Windows ограничена независимо от скорости прикладных клиентов.
if self.closing or not self.serial.isOpen(): return False # Закрытый physical fd не накапливает команды для будущего запуска.
if self.serial.bytesToWrite()+len(data)>262144 or self.serial.write(data)!=len(data): self.fail('Переполнена очередь общего USB; уменьшите поток CAN/Modbus'); return False # Предел 256 КиБ не допускает бесконечного отставания от UART.
return True # Реальное подтверждение операции остаётся обязанностью прикладного протокола.
def open_channel(self,channel): # Каждый backend соединяется отдельно внутри уже работающего общего helper.
if not channel.closed and self.ready: self.send(f'O{channel.number:02X}:{channel.port}\n'.encode('ascii')) # Номер и порт сформированы из проверенных локальных чисел.
def receive(self): # Произвольные USB фрагменты собираются в ограниченные ASCII строки обрамления.
if self.closing: return # Во время короткого cleanup принятые байты проверяет finish, избегая рекурсии callback.
self.incoming.extend(bytes(self.serial.readAll())) # Физический поток читается ровно одним QObject.
if len(self.incoming)>16384: self.fail('Неверный поток общего USB-туннеля'); return # Ни shell, ни повреждённый backend не могут бесконечно накапливать данные.
if not self.ready and self.phase=='shell': # Login и prompt не обязаны оканчиваться переводом строки, поэтому проверяются до разбора multiplex строк.
if b'Password:' in self.incoming: self.fail('Консоль платы требует пароль: войдите через терминал и повторите подключение'); return # Пароль не угадывается и не отправляется автоматически.
if b'\x1b[6n' in self.incoming and not self.cursor_replied: self.cursor_replied=True; self.send(b'\x1b[24;80R') # Factory autologin вызывает resize с ANSI запросом курсора; стандартный ответ 24 строки и 80 столбцов освобождает shell без ввода фиктивной команды.
plain=re.sub(rb'\x1b(?:\[[0-?]*[ -/]*[@-~]|[78])',b'',bytes(self.incoming)) # ANSI save/restore cursor и resize не являются частью реального root prompt.
if re.search(rb'(?:^|[\r\n])root@image_7020:[^\r\n]*#[ \t]*$',plain): self.start_helper(); return # Только фактическая root shell разрешает запуск общего helper после штатного factory autologin.
if re.search(rb'login:[ \t]*$',plain) and not self.login_sent: self.login_sent=True; self.incoming.clear(); self.send(b'root\r') # Известное имя отправляется только getty, действительно ожидающему ввод; строка automatic login не вызывает лишнюю root команду.
return # Текст getty не попадает в Modbus/CAN parser и не считается banner helper.
while b'\n' in self.incoming: # Одно уведомление может содержать ответы нескольких логических каналов.
raw,_,tail=self.incoming.partition(b'\n'); self.incoming[:]=tail; line=raw.rstrip(b'\r') # Терминальные CR не входят в HEX данные прикладных протоколов.
if not self.ready: # Вывод shell и echo стартовой команды не становятся Modbus ответом.
if line==b'#ZYNQ-LINK 1': self.ready=True; self.timer.stop(); [self.open_channel(channel) for channel in list(self.channels.values())] # Только настоящий banner разрешает OPEN backend каналов.
continue # Старый консольный вывод не передаётся ни одному прикладному parser.
if len(line)<3: continue # Пустые строки не влияют на состояние готовых каналов.
try: number=int(line[1:3],16) # Однобайтовый номер позволяет выбрать единственного получателя.
except ValueError: self.fail('Повреждено обрамление общего USB'); return # Ошибка multiplex framing закрывает физическую сессию без повторов.
channel=self.channels.get(number) # Закрытый потребитель может оставить поздние байты, которые следует игнорировать.
if channel is None: continue # Поздний DATA не создаёт сессию автоматически.
if line[:1]==b'O' and len(line)==3: channel.active=True; channel.opened.emit() # Connected относится к установленному loopback соединению конкретной службы.
elif line[:1]==b'C' and len(line)==3: channel.failed('Служба платы закрыла USB-канал') # FIN одного сервиса не отключает остальные протоколы.
elif line[:1]==b'E' and line[3:4]==b':': channel.failed('Ошибка USB-канала службы: errno '+line[4:].decode('ascii','replace')) # Числовая причина Linux доступна рядом с соответствующей вкладкой.
elif line[:1]==b'D' and line[3:4]==b':': # Прикладные данные раскодируются только после выбора активного канала.
try: data=bytes.fromhex(line[4:].decode('ascii')) # Все управляющие значения байта остаются неизменными данными TCP потока.
except (ValueError,UnicodeError): self.fail('Повреждены HEX данные общего USB'); return # Неисправное обрамление не проходит к Modbus C-кодеку как частичный успех.
if len(channel.incoming)+len(data)>65536: channel.failed('Переполнен буфер USB-канала'); continue # Ограничение одного потребителя не мешает работе остальных.
channel.incoming.extend(data); channel.readyRead.emit() # Существующий parser получает произвольный фрагмент исходного TCP.
else: self.fail('Неизвестный ответ общего USB-туннеля'); return # Неожиданное возвращение shell не выдаётся за действующий протокольный поток.
def serial_error(self,error): # Qt NoError не считается отказом физического FTDI.
if not self.closing and self.serial.isOpen() and error not in (QSerialPort.SerialPortError.NoError,QSerialPort.SerialPortError.TimeoutError): self.fail(self.serial.errorString()) # Отключение кабеля завершает все каналы одной общей сессии.
def release(self,channel): # Канал удаляется до посылки CLOSE, исключая повторную обработку поздних данных.
self.channels.pop(channel.number,None) # Повторный cleanup не затрагивает другой номер сессии.
if self.ready and not self.closing: self.send(f'C{channel.number:02X}\n'.encode('ascii')) # Backend получает штатный обрыв собственного TCP соединения.
if not self.channels: self.finish() # Последняя страница возвращает обычную Linux-консоль и освобождает COM.
def fail(self,message): # Физическая ошибка закрывает всех владельцев без автоматического повторного запуска.
if self.closing: return # Синхронный QSerialPort error во время close не вызывает рекурсию.
channels=list(self.channels.values()); self.closing=True # Сначала запрещаются новые записи всех потребителей.
for channel in channels: channel.failed(message) # Каждая вкладка получает собственный cleanup и прекращает опрос.
self.finish() # Общий UART освобождается после доставки ошибок всем потребителям.
def finish(self): # Только последний потребитель завершает собственный foreground helper.
self.closing=True; self.timer.stop() # Остальные Qt callbacks не читают tty во время подтверждения возврата shell.
if self.serial.isOpen(): # Отсоединённый физический кабель не заставляет ждать несуществующее подтверждение.
self.serial.readAll(); self.serial.write(b'\x03'); self.serial.flush() # ISIG остаётся включённым, поэтому Ctrl-C завершает только собственную foreground группу helper.
if self.serial.bytesToWrite(): self.serial.waitForBytesWritten(150) # Очередь передачи ограничена коротким ожиданием фактической отправки Ctrl-C.
elapsed=QElapsedTimer(); elapsed.start(); response=bytearray() # Консоль должна подтвердить выполнение stty sane за ограниченные 500 мс.
while elapsed.elapsed()<500 and not self.console_restored: self.serial.waitForReadyRead(25); response.extend(bytes(self.serial.readAll())); self.console_restored=b'\n#ZYNQ-LINK-CLOSED\r' in response or b'\n#ZYNQ-LINK-CLOSED\n' in response # Собственный marker отличает реальный возврат shell от простого закрытия COM.
self.serial.close() # Сторонний терминал может открыть COM после завершения последнего протокола.
if self.links.get(self.key) is self: self.links.pop(self.key) # Новый запрос пользователя создаст новую чистую физическую сессию.
self.deleteLater() # Удаление QObject выполняется штатным Qt event loop до выгрузки runtime.
File diff suppressed because one or more lines are too long
+102
View File
@@ -0,0 +1,102 @@
"""Асинхронный J-Link-порт: backup, запись, readback, проверка и отдельный reset/run.""" # Внешний процесс не блокирует цикл событий SETGUI.
from pathlib import Path # Артефакты одной операции хранятся вместе в отдельном каталоге.
import re # Итоговый запуск подтверждается выполненной командой g, а не одним кодом возврата.
import tempfile # Уникальный каталог исключает перезапись предыдущей резервной копии.
try: # Общая библиотека работает с современным Qt и существующими сборками Windows 7.
from PySide6.QtCore import QObject, QProcess, QTimer, Signal # QProcess обслуживает stdout, завершение и отмену без потоковой блокировки.
except ImportError: # PySide2 сохраняет тот же публичный контракт сигналов этого порта.
from PySide2.QtCore import QObject, QProcess, QTimer, Signal # Никакой зависимости от внутреннего устройства SETGUI здесь нет.
from set_devices.stm32_jtag import encode_hex, programming_script, start_script, verify_readback # Вся подготовка и проверка сосредоточены в переносимом модуле templates.
class Stm32JtagProgrammer(QObject): # Владеет одной аппаратной операцией до полного завершения обоих этапов.
progress = Signal(int, str) # Основная форма показывает этапы рядом с существующей полосой прогресса.
finished = Signal(bool, str) # Единственный итог освобождает форму также при ошибке запуска executable.
log = Signal(str) # Полный вывод Commander доступен в терминале и сохраняется на диск.
def __init__(self, parent=None): # Создание порта не открывает программатор и не обращается к USB.
super().__init__(parent) # Время жизни порта соответствует его Qt-владельцу.
self._process = None # None означает отсутствие запущенного Commander.
self._active = False # Флаг защищает от повторного finished после errorOccurred и finished одного процесса.
self._timer = QTimer(self) # Ограничивает зависшую внешнюю утилиту без ожидания в GUI.
self._timer.setSingleShot(True) # На один этап приходится один предельный тайм-аут.
self._timer.timeout.connect(lambda: self._stop('Истекло время ожидания J-Link; проверьте журнал')) # Остановка не объявляет частично записанный образ успешным.
@property # Форма может проверить занятость перед повторным нажатием или закрытием.
def active(self): # Состояние охватывает программирование, независимую проверку и reset/run.
return self._active # Между двумя процессами нельзя начать конкурирующую запись.
def start(self, executable, options, segments, backup_root): # Принимает уже проверенный неизменяемый план, подтверждённый пользователем.
if self._active: raise ValueError('J-Link уже выполняет операцию') # Второй запуск не подменяет исходный путь backup и цель MCU.
options.validate() # Защищает прямые вызовы порта вне стандартной формы.
self._options, self._segments, self._executable = options, segments, str(executable) # Копирует параметры текущей операции до запуска QProcess.
self._active, self._stop_reason = True, '' # Обозначает владение программатором и отсутствие запроса отмены.
try: # Ошибки каталога и снимка должны завершать операцию штатным сигналом формы.
root = Path(backup_root).resolve() # Выбранный каталог не зависит от текущей рабочей папки процесса GUI.
root.mkdir(parents=True, exist_ok=True) # Создаёт только папку артефактов пользователя, без установки инструментов.
self.folder = Path(tempfile.mkdtemp(prefix='stm32-jtag-', dir=root)) # Предыдущие backup остаются нетронутыми даже при повторной неудачной попытке.
(self.folder / 'image.hex').write_text(encode_hex(segments), encoding='utf-8') # Фиксирует проверенные адресованные данные, исключая изменение исходника во время записи.
self.log.emit(f'Артефакты J-Link и резервная копия: {self.folder}') # Путь известен оператору до первого аппаратного действия.
self.progress.emit(0, 'Резервная копия, запись и чтение Flash') # Число отражает этап, поскольку Commander сам выводит ход программирования в терминал.
self._launch('program', programming_script(self.folder, options)) # Сначала исполняется только этап с остановленным ядром и двумя SaveBin.
except (OSError, ValueError) as error: self._finish(False, str(error)) # Отказ файловой системы не оставляет интерфейс заблокированным.
def _launch(self, stage, commands): # Каждый этап использует новый QProcess и собственный проверяемый command file.
self._stage, self._output = stage, '' # Вывод текущего этапа нужен также для подтверждения факта команды запуска.
script = self.folder / (stage + '.jlink') # Точный выполненный сценарий сохраняется рядом с backup.
script.write_text(commands, encoding='utf-8') # UTF-8 без BOM совместим с установленным Commander.
process = QProcess(self) # QObject-владелец удерживает процесс до обработки сигнала finished.
self._process = process # Отмена и тайм-аут относятся только к текущему этапу.
process.setProcessChannelMode(QProcess.MergedChannels) # Ошибки stderr сохраняются в том же порядке, что служебные сообщения stdout.
process.readyReadStandardOutput.connect(self._read_output) # Строки отображаются по мере поступления без waitForFinished.
process.errorOccurred.connect(self._process_error) # Отсутствующий executable и невозможность запуска дают понятный итог.
process.finished.connect(self._process_finished) # Только штатное завершение запускает независимую проверку и следующий этап.
self._timer.start(300000) # Пять минут на этап покрывают медленный JTAG; GUI продолжает обрабатывать отмену.
process.start(self._executable, self._options.arguments() + ['-Log', str(self.folder / (stage + '-dll.log')), '-CommanderScript', str(script)]) # Путь, серийный номер и файл передаются отдельными аргументами без командной оболочки.
def _read_output(self): # Вызывается также при finished, чтобы не потерять последний фрагмент stdout.
if self._process is None: return # Сигнал уже освобождённого этапа не должен обращаться к новому процессу.
chunk = bytes(self._process.readAllStandardOutput()).decode('utf-8', errors='replace').replace('\b', '') # Управляющие backspace процентов Commander не ломают текстовый терминал.
self._output += chunk # Полный вывод сохраняется для анализа результата и диагностики ошибки.
if chunk: self.log.emit(chunk.rstrip()) # Отображает сообщения сразу, включая отсутствие питания и занятой J-Link.
def _process_error(self, error): # FailedToStart не сопровождается finished и требует самостоятельного завершения.
if self._active and error == QProcess.FailedToStart: self._finish(False, 'Не удалось запустить J-Link: ' + self._process.errorString()) # Не оставляет активный таймер после неверного пути executable.
def _process_finished(self, code, status): # Последовательно выполняет readback-проверку и отдельный reset/run.
if not self._active: return # Игнорирует повторное уведомление ранее завершённой ошибки.
self._timer.stop() # Проверка локальных файлов не должна срабатывать как тайм-аут уже завершённого Commander.
self._read_output() # Забирает остаток вывода перед сохранением журнала.
try: # Все ошибки проверки и записи журнала превращаются в один finished(False).
(self.folder / (self._stage + '.log')).write_text(self._output, encoding='utf-8') # Читаемый журнал содержит именно тот вывод, который видел оператор.
process, self._process = self._process, None # Старый QObject больше не считается владельцем текущего этапа.
process.deleteLater() # Удаление выполняется циклом Qt после выхода из обработчика finished.
if self._stop_reason: raise ValueError(self._stop_reason) # Отмена никогда не переходит к командам reset/run.
if code != 0 or status != QProcess.NormalExit: raise ValueError(f'J-Link завершился с ошибкой {code}; журнал: {self.folder}') # Успех требует нормального завершения внешней утилиты.
if self._stage == 'program': # Даже код ноль не подтверждает, что интерактивное подключение реально дошло до записи.
before, after = (self.folder / 'before.bin').read_bytes(), (self.folder / 'after.bin').read_bytes() # Отсутствующий SaveBin автоматически исключает ложный успех.
verify_readback(self._segments, self._options, before, after) # Проверяет реальные байты записанных диапазонов и защищённую страницу UKSVEP.
self.progress.emit(90, 'Flash проверена, сброс и запуск STM32') # Переход к запуску разрешён только после независимой проверки обоих снимков.
self._launch('run', start_script(self._options)) # Новый Commander больше не стирает и не программирует Flash.
else: # Второй этап должен дойти до явного возобновления исполнения ядра.
if not re.search(r'J-Link>\s*g\s*(?:\r?\n|$)', self._output): raise ValueError('J-Link не подтвердил выполнение команды запуска; проверьте журнал') # Преждевременный выход с кодом ноль не превращается в сообщение о работающем MCU.
self.progress.emit(100, 'Запись и проверка завершены, выполнен reset/run') # Проценты завершаются только после обоих этапов.
self._finish(True, f'STM32 прошит через {self._options.interface}; Flash проверена, reset/run выполнен. Backup: {self.folder / "before.bin"}') # Итог содержит доступную для восстановления резервную копию.
except (OSError, ValueError) as error: self._finish(False, str(error)) # При отказе проверки ядро не запускается новым этапом.
def cancel(self): # Вызывается штатной кнопкой отмены формы прошивки.
self._stop('J-Link: операция отменена; частичная Flash требует повторной прошивки, резервная копия сохранена в каталоге операции') # Не обещает целый образ после прерывания аппаратной записи.
def _stop(self, reason): # Общая остановка обслуживает кнопку отмены и предельный тайм-аут.
if not self._active: return # Повторная отмена завершённого порта ничего не меняет.
self._stop_reason = reason # finished процесса должен сохранить причину вместо сообщения об обычном ненулевом коде.
if self._process is not None: self._process.kill() # Останавливает только запущенный этим портом Commander, не затрагивая Keil и другие программаторы.
else: self._finish(False, reason) # Ошибка между этапами также освобождает форму без дополнительного процесса.
def _finish(self, success, message): # Единственная точка завершения защищает форму от двойных уведомлений.
if not self._active: return # Второй сигнал ошибки того же QProcess не открывает повторное окно.
self._active = False # Новая операция разрешается только после окончательного результата текущей.
self._timer.stop() # Поздний тайм-аут не должен отменить следующий пользовательский запуск.
if self._process is not None: self._process.deleteLater() # FailedToStart не использует общий обработчик finished.
self._process = None # Освобождает ссылку на завершённый QObject.
self.finished.emit(success, message) # Владелец разблокирует форму и покажет итог стандартным механизмом SETGUI.
@@ -0,0 +1,96 @@
"""Общие настройки STM32/J-Link для встраивания в существующий экран прошивки.""" # SETGUI предоставляет лишь выбранный образ и свой терминал.
from pathlib import Path # Проверяет путь внешнего инструмента до подтверждения аппаратной операции.
try: # Поддерживает обе используемые SETGUI ветви Qt без зависимости от gui_desktop.
from PySide6.QtCore import QCoreApplication, QSettings, QStandardPaths, Signal # Настройки и каталог backup принадлежат текущему приложению.
from PySide6.QtWidgets import QWidget, QFormLayout, QHBoxLayout, QVBoxLayout, QLineEdit, QComboBox, QSpinBox, QCheckBox, QPushButton, QFileDialog, QLabel # Использует обычные виджеты, доступные в Qt 5 и Qt 6.
except ImportError: # Старые Windows-сборки сохраняют тот же публичный интерфейс панели.
from PySide2.QtCore import QCoreApplication, QSettings, QStandardPaths, Signal # Не навязывает основному приложению версию Qt.
from PySide2.QtWidgets import QWidget, QFormLayout, QHBoxLayout, QVBoxLayout, QLineEdit, QComboBox, QSpinBox, QCheckBox, QPushButton, QFileDialog, QLabel # Набор контролов одинаков для обеих реализаций.
from set_devices.stm32_jtag import JLinkOptions, find_jlink, prepare_image # Общий план выполняет все проверки MCU, адресов и manifest до записи.
from .stm32_jtag import Stm32JtagProgrammer # Порт QProcess отвечает за журнал, отмену и оба этапа операции.
class Stm32JtagPanel(QWidget): # Встраивается в FirmwareTab вместо UART/CAN-настроек выбранного режима.
progress = Signal(int, str) # Перенаправляет этапы в полосу прогресса основной формы.
finished = Signal(bool, str) # Результат обрабатывается тем же методом, что остальные загрузчики.
log = Signal(str) # Вывод J-Link направляется в существующий терминал прошивки.
def __init__(self, parent=None, settings=None): # Явное хранилище позволяет изолировать настройки в тестах и других приложениях.
super().__init__(parent) # Панель следует обычному времени жизни владельца QWidget.
self.settings = settings if settings is not None else QSettings() # Не подменяет тестовый профиль глобальным пользовательским реестром.
self.programmer = Stm32JtagProgrammer(self) # Создание формы само по себе не подключает USB и не запускает Commander.
self.programmer.progress.connect(self.progress.emit) # Прогресс проходит через общий публичный интерфейс панели.
self.programmer.finished.connect(self.finished.emit) # Ошибки и успех освобождают внешнюю форму одинаковым способом.
self.programmer.log.connect(self.log.emit) # Терминал получает и служебные строки, и сообщения SEGGER.
if QCoreApplication.instance() is not None: QCoreApplication.instance().aboutToQuit.connect(self.programmer.cancel) # Закрытие приложения не оставляет скрытый процесс программирования без владельца.
root = QVBoxLayout(self) # Все настройки образуют одну колонку внутри существующей левой панели.
root.setContentsMargins(0, 0, 0, 0) # Внешние отступы уже заданы формой прошивки.
hint = QLabel('J-Link через JTAG или SWD. Доступ к BOOT0 не требуется. Перед записью сохраняется полная резервная копия Flash.') # Оператор сразу видит назначение режима и способ первого программирования.
hint.setWordWrap(True) # Описание помещается в узкую колонку рядом с терминалом.
root.addWidget(hint) # Пояснение располагается над параметрами подключения.
form = QFormLayout() # Подписи не смешиваются с UART-полями других загрузчиков.
self.executable = QLineEdit(str(self._value('executable', find_jlink()))) # Позволяет использовать SEGGER из Keil либо отдельную установленную версию.
executable_row = QHBoxLayout() # Кнопка выбора остаётся рядом с полным путём инструмента.
executable_row.addWidget(self.executable) # Введённый путь передаётся QProcess отдельным аргументом.
executable_button = QPushButton('Обзор…') # Поиск установленного инструмента не требует скачивания и установки.
executable_button.clicked.connect(self._choose_executable) # Диалог выбирает только путь, без запуска программатора.
executable_row.addWidget(executable_button) # Кнопка сохраняет естественную компактную ширину.
form.addRow('J-Link Commander', executable_row) # Имя executable явно отделено от названия MCU.
self.device = QComboBox() # Стандартные имена подходят базе устройств установленного Commander.
self.device.setEditable(True) # Другой STM32 можно указать вручную вместе с фактическим объёмом Flash.
self.device.addItems(['STM32F103RC', 'STM32F103C8', 'STM32F103ZE', 'STM32F407VE', 'STM32G474VE']) # Список охватывает используемые проектами templates семейства.
self.device.setCurrentText(str(self._value('device', 'STM32F103RC'))) # По умолчанию выбирается текущая плата, ранее проверенная через JTAG.
form.addRow('Микроконтроллер', self.device) # Полное имя цели видно до подтверждения записи.
self.interface = QComboBox() # Интерфейс не зависит от UART-скорости верхней панели SETGUI.
self.interface.addItems(['JTAG', 'SWD']) # Оба варианта используют J-Link, а не встроенный ROM Boot.
self.interface.setCurrentText(str(self._value('interface', 'JTAG'))) # Исходное подключение текущей платы выполнено по JTAG.
form.addRow('Интерфейс', self.interface) # Пользователь видит выбранный физический способ подключения.
self.serial = QLineEdit(str(self._value('serial', ''))) # Не закрепляет серийный номер чужого программатора в общей библиотеке.
self.serial.setPlaceholderText('Необязательно, если подключён один J-Link') # Несколько программаторов можно однозначно различить по серийному номеру.
form.addRow('Серийный номер J-Link', self.serial) # Поле соответствует штатному параметру Commander -USB.
self.speed = QSpinBox() # Частота задаётся числом с явными единицами.
self.speed.setRange(1, 50000) # Допускает медленную плату и быстрые поддерживаемые подключения.
self.speed.setValue(int(self._value('speed', 1000))) # Проверенная начальная частота 1000 кГц подходит текущему JTAG.
self.speed.setSuffix(' кГц') # Исключает смешение TCK/SWCLK со скоростью COM в бодах.
form.addRow('Частота JTAG/SWD', self.speed) # Значение передаётся напрямую в -speed.
self.flash_size = QSpinBox() # Полный объём нужен для сохранения Flash перед любым программированием.
self.flash_size.setRange(16, 4096) # Интервал покрывает поддерживаемый переносимым планом объём 16 КиБ..4 МиБ.
self.flash_size.setValue(int(self._value('flash_size', 256))) # У STM32F103RC установлены 256 КиБ внутренней Flash.
self.flash_size.setSuffix(' КиБ') # Длина SaveBin вычисляется из этого значения в байтах.
form.addRow('Объём Flash', self.flash_size) # Для другого MCU оператор должен указать его фактический объём.
self.uksvep = QCheckBox('UKSVEP F103RC: сохранить параметры и обновить CRC приложения') # Явный профиль разрешает специфические адреса manifest и регистров BKP.
self.uksvep.setChecked(self.settings.value('firmware/stm32-jtag/uksvep', True, type=bool)) # Выбор сохраняется независимо от общего имени MCU.
self.uksvep.setToolTip('Для другой платы снимите этот флажок. UKSVEP принимает полный app с 0x08008000 или установочный HEX boot+app.') # Объясняет действие профиля при осмысленном выборе оператором.
form.addRow('Профиль платы', self.uksvep) # Обычные STM32 не наследуют защиту и служебную запись UKSVEP автоматически.
default_backup = str(Path(QStandardPaths.writableLocation(QStandardPaths.AppLocalDataLocation)) / 'stm32-jtag') # Каталог остаётся записываемым также у установленного в Program Files приложения.
self.backup_root = QLineEdit(str(self._value('backup_root', default_backup))) # Каждая операция создаёт внутри отдельную подпапку с образами и журналами.
backup_row = QHBoxLayout() # Настройка архива не требует ручного редактирования путей Windows.
backup_row.addWidget(self.backup_root) # Полный путь остаётся доступным для копирования оператором.
backup_button = QPushButton('Обзор…') # Выбирает существующий базовый каталог резервных копий.
backup_button.clicked.connect(self._choose_backup) # Диалог не изменяет Flash и настройки программатора.
backup_row.addWidget(backup_button) # Действие располагается у соответствующего пути.
form.addRow('Резервные копии', backup_row) # Backup включён всегда и не прячется в настройках UART.
root.addLayout(form) # Готовая панель встраивается без собственной кнопки прошивки и повторной полосы прогресса.
def _value(self, name, default): # Все ключи принадлежат отдельному стабильному разделу прошивки J-Link.
return self.settings.value('firmware/stm32-jtag/' + name, default) # Прежние UART/CAN-настройки не меняются.
def _choose_executable(self): # Пользователь явно указывает уже установленный J-Link Commander.
path, _ = QFileDialog.getOpenFileName(self, 'J-Link Commander', self.executable.text(), 'J-Link (JLink.exe JLinkExe);;Все файлы (*)') # Расширение не ограничивает Linux-версию SEGGER.
if path: self.executable.setText(path) # Отмена диалога сохраняет текущий путь.
def _choose_backup(self): # Позволяет хранить backup вне каталога установки GUI и исходников.
path = QFileDialog.getExistingDirectory(self, 'Каталог резервных копий STM32', self.backup_root.text()) # Выбор каталога не запускает аппаратную операцию.
if path: self.backup_root.setText(path) # Прежний архив остаётся выбранным при отмене диалога.
def prepare(self, image): # Основная форма вызывает проверку до стандартного подтверждения записи.
executable = Path(self.executable.text().strip()) # Путь не интерпретируется как команда оболочки.
if not executable.is_file(): raise ValueError('Выберите установленный J-Link Commander (JLink.exe)') # Отказывает до блокировки формы и открытия программатора.
if not self.backup_root.text().strip(): raise ValueError('Выберите каталог резервных копий Flash') # Не сохраняет обязательный backup в непредсказуемый текущий каталог.
options = JLinkOptions(self.device.currentText().strip(), self.interface.currentText(), self.speed.value(), self.serial.text().strip(), self.flash_size.value(), self.uksvep.isChecked()) # Фиксирует все параметры одновременно с неизменяемым образом.
return str(executable), options, prepare_image(image, options), self.backup_root.text().strip() # Возвращает точный план для единственного последующего подтверждённого запуска.
def start(self, job): # Вызывается только после существующего подтверждения прошивки в FirmwareTab.
for name, widget in (('executable', self.executable), ('serial', self.serial), ('backup_root', self.backup_root)): self.settings.setValue('firmware/stm32-jtag/' + name, widget.text().strip()) # Сохраняет пути и идентификатор без доступа к остальным настройкам GUI.
for name, value in (('device', self.device.currentText()), ('interface', self.interface.currentText()), ('speed', self.speed.value()), ('flash_size', self.flash_size.value()), ('uksvep', self.uksvep.isChecked())): self.settings.setValue('firmware/stm32-jtag/' + name, value) # Следующий запуск формы восстанавливает фактическое последнее подключение.
self.programmer.start(*job) # Процесс получает уже проверенный снимок, а не повторно читает изменяемые поля формы.
+124
View File
@@ -0,0 +1,124 @@
"""Подготовка и независимая проверка J-Link-прошивки STM32 без зависимости от Qt.""" # Один план используют SETGUI и консольные клиенты templates.
from dataclasses import dataclass # Неизменяемые настройки фиксируют назначение до первой записи Flash.
from pathlib import Path # Пути передаются программатору как данные, без выполнения оболочкой.
import os # Стандартные каталоги Windows позволяют найти уже установленный J-Link.
import re # Ограничивает поля, попадающие в аргументы J-Link Commander.
import shutil # Поиск PATH сохраняет возможность использовать установленный SEGGER вне Keil.
import struct # Manifest UKSVEP содержит шесть little-endian слов uint32.
import zlib # CRC32 IEEE совпадает с валидатором production-загрузчика UKSVEP.
from firmware_image.core import HexSegment, parse_intel_hex, contiguous_image # Проверка формата и checksum использует каноническое C-ядро templates.
from .mcu_profiles import UKSVEP_F103RC_BOOT # Адрес приложения, SRAM и Product берутся из общего профиля платы.
FLASH_BASE = 0x08000000 # Внутренняя Flash STM32 отображается с этого адреса в карте памяти Cortex-M.
UKS_APP = UKSVEP_F103RC_BOOT['app_base'] # Начальные 32 КиБ UKSVEP занимает постоянный загрузчик.
UKS_PARAMS = UKS_APP + UKSVEP_F103RC_BOOT['app_size'] # Следующая страница 0x0801F800 хранит параметры и исключается из записи.
UKS_META = 0x0803F800 # Последняя страница 256-КиБ Flash содержит production-manifest, см. boot_profile.h UKSVEP.
@dataclass(frozen=True) # Значения нельзя незаметно изменить между подтверждением формы и запуском процесса.
class JLinkOptions: # Описывает конкретный STM32 и физическое подключение программатора.
device: str = 'STM32F103RC' # Имя совпадает с базой устройств J-Link; корпусный суффикс T6 не требуется.
interface: str = 'JTAG' # SWD использует тот же Commander и выбирается пользователем отдельно.
speed_khz: int = 1000 # Частота TCK/SWCLK задаётся в кГц и не связана со скоростью UART.
serial: str = '' # Пустое значение разрешает штатный выбор единственного подключённого J-Link.
flash_kib: int = 256 # Размер всей Flash задаёт длину обязательных backup и readback.
uksvep: bool = True # Только явно выбранный профиль UKSVEP включает защиту параметров и manifest.
def validate(self): # Отказ происходит до создания command file и доступа к плате.
if not re.fullmatch(r'STM32[A-Za-z0-9_]+', self.device): raise ValueError('Укажите имя STM32 из базы J-Link') # Не допускает пробелы и команды вместо идентификатора MCU.
if self.interface not in ('JTAG', 'SWD'): raise ValueError('Выберите JTAG или SWD') # Остальные интерфейсы не относятся к этой панели.
if not 1 <= self.speed_khz <= 50000: raise ValueError('Частота JTAG/SWD должна быть 1..50000 кГц') # Верхняя граница соответствует доступному диапазону формы.
if self.serial and not re.fullmatch(r'[0-9]+', self.serial): raise ValueError('Серийный номер J-Link должен содержать только цифры') # Идентификатор не должен включать дополнительные аргументы.
if not 16 <= self.flash_kib <= 4096: raise ValueError('Размер Flash должен быть 16..4096 КиБ') # Ограничивает аппаратное чтение выбранным пользователем объёмом STM32.
if self.uksvep and (self.device != 'STM32F103RC' or self.flash_kib != 256): raise ValueError('Профиль UKSVEP требует STM32F103RC и Flash 256 КиБ') # Не применяет карту F103RC и регистры BKP к другому семейству.
def arguments(self): # Возвращает отдельные аргументы QProcess без shell=True и строковой сборки команды.
self.validate() # Публичный метод безопасен также вне GUI.
args = ['-device', self.device, '-if', self.interface, '-JTAGConf', '-1,-1', '-speed', str(self.speed_khz), '-autoconnect', '1', '-ExitOnError', '1', '-NoGui', '1'] # Явная цепочка исключает интерактивный JTAGConf, преждевременно завершавший batch с кодом ноль.
return (['-USB', self.serial] if self.serial else []) + args # Серийный номер закрепляет конкретный программатор при нескольких подключениях.
def find_jlink(): # Обнаруживает существующий инструмент и ничего не скачивает в систему.
found = shutil.which('JLink.exe') or shutil.which('JLinkExe') # PATH может содержать также одноимённый сборщик модулей Java JDK.
candidates = ['C:/Keil_v5_41/ARM/Segger/JLink.exe', 'C:/Keil_v5/ARM/Segger/JLink.exe'] # Проверенные каталоги SEGGER в Keil имеют приоритет над неоднозначным PATH.
for variable in ('ProgramFiles', 'ProgramFiles(x86)'): # SEGGER может быть установлен в 32- или 64-разрядный каталог Windows.
if os.environ.get(variable): candidates.append(str(Path(os.environ[variable]) / 'SEGGER/JLink/JLink.exe')) # Не читает реестр и не требует прав администратора.
if found and not any((Path(found).parent / name).is_file() for name in ('java.exe', 'java', 'jmod.exe', 'jmod')): candidates.append(found) # Инструмент из bin Java JDK не является программатором SEGGER и не должен выбираться автоматически.
return next((str(Path(item)) for item in candidates if item and Path(item).is_file()), '') # Пустой результат предлагает явный выбор executable в форме.
def prepare_image(image, options): # Проверяет выбранный ранее FirmwareImage и сохраняет разреженную адресацию HEX.
options.validate() # Настройки проверяются до чтения внешних файлов и доступа к программатору.
path = Path(image.path) # Образ может прийти как из формы, так и из Публикатора.
if path.stat().st_size > 16 * 1024 * 1024: raise ValueError('Файл прошивки превышает 16 МиБ') # Ограничение покрывает 4 МиБ данных Intel HEX с текстовыми накладными расходами.
raw = path.read_bytes() # Читает единственный снимок исходника, который затем сравнивается с подтверждённым выбором.
if path.suffix.lower() == '.hex': # HEX уже содержит абсолютные адреса всех отдельных диапазонов Flash.
flat, base = contiguous_image(raw, options.flash_kib * 1024) # Канонический parser проверяет checksum, пересечения и полный адресный размах.
if flat != image.data or base != image.base_address: raise ValueError('Файл или адрес HEX изменён после выбора; выберите образ заново') # Не записывает новый файл под видом ранее выбранного образа и не смещает HEX.
segments = parse_intel_hex(raw) # Разрывы остаются отсутствующими записями, а не превращаются в байты FF для loadfile.
elif path.suffix.lower() == '.bin': # BIN требует явно указанного адреса, поскольку сам его не хранит.
if raw != image.data: raise ValueError('BIN изменён после выбора; выберите образ заново') # Не использует устаревший SHA-256 формы для другого содержимого.
segments = [HexSegment(image.base_address, raw)] # Адрес из формы относится к началу полезных байтов BIN.
else: raise ValueError('Для J-Link выберите Intel HEX или BIN') # OUT/ELF не обходят единый проверяемый план записи Flash.
if not segments or any(not part.data or part.start < FLASH_BASE or part.end >= FLASH_BASE + options.flash_kib * 1024 for part in segments): raise ValueError('Образ выходит за выбранный объём Flash STM32') # Не допускает запись RAM, option bytes или соседней памяти.
if options.uksvep: # Защита и manifest относятся только к конкретному порту UKSVEP.
if any(part.start < UKS_PARAMS + 2048 and part.end >= UKS_PARAMS for part in segments): raise ValueError('Образ затрагивает страницу параметров UKSVEP 0x0801F800') # Размер страницы F103RC равен 2048 байтам по профилю boot.
app = [part for part in segments if part.start < UKS_PARAMS and part.end >= UKS_APP] # Находит приложение без включения загрузчика и служебной страницы.
if app: # Запись одного app HEX также должна обновить CRC, иначе постоянный boot не разрешит запуск.
if len(app) != 1 or app[0].start != UKS_APP or app[0].end >= UKS_PARAMS: raise ValueError('Для UKSVEP нужен полный непрерывный образ приложения с 0x08008000') # Не выдаёт фрагментарный патч или смешанный BIN за полное приложение.
data = app[0].data # CRC считается по полезной длине приложения, без промежутков Flash.
if len(data) < 8: raise ValueError('В приложении отсутствует таблица векторов Cortex-M3') # Два первых слова содержат MSP и Reset_Handler.
sp, pc = struct.unpack_from('<II', data) # Векторы Cortex-M3 хранятся в little-endian.
if sp & 7 or not 0x20000000 < sp <= UKSVEP_F103RC_BOOT['stack_end'] or not pc & 1 or not UKS_APP <= (pc & ~1) < UKS_APP + len(data): raise ValueError('Неверные векторы приложения UKSVEP') # Требует SRAM 48 КиБ, выравнивание стека и Thumb-вход внутрь образа.
crc = zlib.crc32(data) & 0xFFFFFFFF # IEEE CRC32 совпадает с uks_boot_installed_valid() production-порта.
manifest = struct.pack('<6I', 0x554B5356, len(data), crc, ~len(data) & 0xFFFFFFFF, ~crc & 0xFFFFFFFF, UKSVEP_F103RC_BOOT['product']) # Magic и шесть слов заданы UKS_META_MAGIC/UKS_META_WORDS в boot_port.c.
existing = [part for part in segments if part.start < UKS_META + 2048 and part.end >= UKS_META] # Проверяет также явный manifest установочного HEX.
if existing and (len(existing) != 1 or existing[0].start != UKS_META or existing[0].data != manifest): raise ValueError('Manifest UKSVEP не соответствует приложению') # Не скрывает неверную служебную запись готового комплекта.
if not existing: segments.append(HexSegment(UKS_META, manifest)) # Для app-only JTAG формирует ту же запись активации, что UART/CAN boot.
return tuple(sorted(segments, key=lambda item: item.start)) # Неизменяемый отсортированный снимок используется для loadfile и последующего readback.
def encode_hex(segments): # Сохраняет только фактические диапазоны плана без заполнения защищённых разрывов.
lines, upper = [], None # Старшие шестнадцать бит записываются при каждом переходе между областями 64 КиБ.
def record(address, kind, data): # Стандартная строка Intel HEX содержит длину, адрес, тип, данные и дополнение суммы до нуля.
body = bytes((len(data), address >> 8, address & 255, kind)) + data # Адрес внутри записи ограничен текущим окном 64 КиБ.
return ':' + (body + bytes((-sum(body) & 255,))).hex().upper() # Проверяемая checksum делает снимок пригодным для независимого J-Link parser.
for part in segments: # Каждый диапазон начинает адресованные данные без добавления промежутков.
offset = 0 # Положение выражено в байтах текущего сегмента.
while offset < len(part.data): # Не создаёт пустых записей в конце последнего блока.
address = part.start + offset # Вычисляет абсолютный адрес следующей порции.
if address >> 16 != upper: # Type 04 обновляет старшую половину линейного адреса.
upper = address >> 16 # Последующие data-записи используют именно это окно.
lines.append(record(0, 4, upper.to_bytes(2, 'big'))) # Порядок байтов type 04 задан Intel HEX и не зависит от Cortex-M.
count = min(16, len(part.data) - offset, 0x10000 - (address & 0xFFFF)) # Строка не пересекает границу окна даже при невыровненном начале.
lines.append(record(address & 0xFFFF, 0, part.data[offset:offset + count])) # Добавляет только реальные байты исходного диапазона.
offset += count # Следующая строка продолжает непрерывные данные данного сегмента.
return '\n'.join(lines + [record(0, 1, b'')]) + '\n' # EOF завершает UTF-8/ASCII-снимок с обычными переводами строк.
def quote_path(path): # Экранирование J-Link command file отличается от аргументов системного процесса.
value = Path(path).resolve().as_posix() # Абсолютные пути исключают зависимость backup от рабочего каталога.
if any(char in value for char in ('"', '\r', '\n')): raise ValueError('Путь J-Link не должен содержать кавычки или переносы строк') # Не позволяет имени каталога стать дополнительной командой Commander.
return '"' + value + '"' # Пробелы и кириллица остаются частью одного имени файла.
def programming_script(folder, options): # Первый этап сохраняет backup и readback, оставляя ядро остановленным до независимой проверки.
folder = Path(folder) # Все артефакты принадлежат отдельному каталогу одной операции.
length = hex(options.flash_kib * 1024) # SaveBin принимает длину полного Flash в байтах.
return '\n'.join(['ExitOnError 1', 'r', 'h', f'savebin {quote_path(folder / "before.bin")} 0x08000000 {length}', f'loadfile {quote_path(folder / "image.hex")}', f'savebin {quote_path(folder / "after.bin")} 0x08000000 {length}', 'q', '']) # Полного erase нет; loadfile программирует разреженный проверенный HEX с собственной Verify.
def verify_readback(segments, options, before, after): # Проверяет аппаратные снимки независимо от кода возврата Commander.
if len(before) != options.flash_kib * 1024 or len(after) != len(before): raise ValueError('J-Link не сохранил полный backup/readback Flash; запуск отменён') # Нулевой код интерактивно прерванного Commander не считается успехом.
for part in segments: # Каждая реально записываемая область должна совпасть побайтно.
offset = part.start - FLASH_BASE # Смещение внутри полного аппаратного снимка.
if after[offset:offset + len(part.data)] != part.data: raise ValueError(f'Ошибка проверки Flash по адресу 0x{part.start:08X}; запуск отменён') # Не запускает MCU после неполного программирования.
if options.uksvep: # Проверяет сохранность старых настроек именно там, где их хранит UKSVEP.
offset = UKS_PARAMS - FLASH_BASE # Страница параметров находится между boot/app и manifest.
if before[offset:offset + 2048] != after[offset:offset + 2048]: raise ValueError('Изменилась страница параметров UKSVEP; запуск отменён') # Проверка касается реальной Flash, а не только отсутствия области в HEX.
def start_script(options): # Второй этап выполняется только после успешной проверки readback на хосте.
commands = ['ExitOnError 1', 'r'] # Сброс снимает состояние RAM-алгоритма Flash, оставшееся после loadfile.
if options.uksvep: commands += ['w4 0x4002101C 0x18000000', 'w4 0x40007000 0x00000100', 'w4 0x40006C24 0x00000000', 'w4 0x40006C28 0x00000000'] # F103RC: включение PWR/BKP, DBP bit8 и очистка DR9/DR10 убирают старый одноразовый запрос boot без стирания резервного домена.
return '\n'.join(commands + ['g', 'q', '']) # Возобновляет выполнение после reset и освобождает программатор без повторной записи Flash.
+51
View File
@@ -0,0 +1,51 @@
"""Эталонные обмены SCI ROM и TI Flash Kernel A без настоящей платы.""" # Проверки не открывают COM и не вызывают внешнюю программу C2Prog.
import struct # Формирует независимые тестовые SCI8-векторы с явно заданными словами и адресами.
import pytest # Проверяет отказ при повреждениях и граничные размеры порций ядра TI.
from protocan.c2000_sci import Session, decode, validate, ulitco_enter # Испытывает настоящий ctypes-порт общего C99-ядра.
def sci8(address, words): # Готовит минимальную адресную таблицу для эталонных данных теста.
pointer = struct.pack("<HH", address >> 16, address & 65535) # SCI8 хранит старшее слово адреса перед младшим.
return b"\xaa\x08" + bytes(16) + pointer + struct.pack("<H", len(words)) + pointer + struct.pack("<" + "H" * len(words), *words) + bytes(2) # Вектор содержит ключ, резерв, entry, одну секцию и терминатор.
@pytest.mark.parametrize("words", [1, 1024, 1025, 2048, 2050]) # Границы проверяют обычную порцию, остаток и дополнительный нулевой ACK официального TI kernel.
def test_complete_echo_and_checksum_transcript(words): # Фактические ответы подтверждают порядок и неизменность всего приложения.
kernel, image = sci8(0x9000, [0x1234, 0x5678]), sci8(0x328000, [0xA55A] * words) # RAM-ядро и Flash занимают разные разрешённые области.
session, received, pauses, zero_acks = Session(kernel, image), bytearray(), [], 0 # Наблюдения не используют внутреннюю структуру C-контекста.
for _ in range(len(kernel) + words + 100): # Конечный предел выявляет зацикливание автомата.
state, tx, expected, delay = session.next() # Проверяет настоящий сформированный C-пакет.
if state == 0: break # Успех допустим лишь после отправки нулевого терминатора приложения.
if state == 2: pauses.append(delay); continue # Внешний транспорт выполняет только заданную алгоритмом задержку.
if expected == 1: session.accept(tx) # ROM и autobaud должны вернуть точное эхо отправленного октета.
else: # Kernel A подтверждает сумму октетов после erase/program, без побайтового эха приложения.
received.extend(tx) # Снимок подтверждает отсутствие пропущенных, повторённых либо изменённых октетов SCI8.
if expected: session.accept(struct.pack("<H", sum(tx) & 65535)) # Независимый эталон суммы следует опубликованному исходнику TI SendCheckSum.
if not tx: zero_acks += 1 # Пустой остаток TI подтверждается отдельным нулевым словом.
else: pytest.fail("Автомат SCI не завершился") # Отсутствие терминального перехода считается ошибкой протокола.
assert bytes(received) == image # MCU получил ровно исходный Flash SCI8 без переименования или подмены содержимого.
assert pauses == [3000, 3000] and session.progress == 100 # Порядок ожиданий совпадает с TI f05_DownloadImage.
assert zero_acks == int(words > 1024 and words % 1024 == 0) # Обрабатывает особенность исходного TI Kernel A на кратной порции.
def test_corrupt_echo_and_checksum_stop_transfer(): # Повреждённое подтверждение не должно разрешать следующий пакет.
kernel, image = sci8(0x9000, [1]), sci8(0x328000, [2]) # Минимальные векторы отделяют проверку ответа от больших файлов.
session = Session(kernel, image); session.next() # Первый пакет требует эхо ASCII A.
with pytest.raises(ValueError, match="не совпали"): session.accept(b"B") # Чужой байт autobaud не принимается как связь с ROM.
session = Session(kernel, image) # Независимый сеанс проверяет повреждение двухоктетной суммы Flash.
while True: # Доходит до первой операции erase/header без использования внутренних фаз автомата.
state, tx, expected, _ = session.next() # Паузы не требуют аппаратного ответа.
if state == 2: continue # Номер стадии остаётся внутри C-ядра.
if expected == 1: session.accept(tx) # Корректный ROM позволяет загрузить RAM-ядро.
else: break # Первый двухоктетный ответ должен подтвердить заголовок приложения.
with pytest.raises(ValueError, match="не совпали"): session.accept(b"\xff\xff") # Неверная сумма не разрешает программирование следующей секции.
def test_ascii_decode_memory_ranges_and_monitor_command(): # Проверяет переносимость форматов и аппаратные границы до открытия порта.
image = sci8(0x328000, [0x1234]) # Обычный Flash-вектор не пересекает CSM.
ascii_image = b"\x02\n" + " ".join(f"{value:02X}" for value in image).encode("ascii") + b"\n\x03\n" # Формат соответствует выводу TI hex2000 -boot -sci8 -a.
assert decode(ascii_image) == image and validate(image) == (0x328000, 4) # Сектор C имеет маску 0x0004 в TI Flash API.
with pytest.raises(ValueError): decode(ascii_image[:-2]) # Усечённый ETX не должен оставить частично декодированное ядро.
with pytest.raises(ValueError): validate(sci8(0x33FFF8, [0x1234])) # Область CSM-паролей запрещена независимо от имени файла.
with pytest.raises(ValueError): validate(sci8(0x8000, [0x1234])) # RAM-образ не может стать Flash-приложением из-за выбранного loader.
assert ulitco_enter() == bytes([247, 213, 255, 93, 0, 0, 1, 0, 93, 1]) # Эталон совпадает с десятиоктетным Monitor-запросом FLASH_NUM=93 текущей прошивки UlitCo.
+99
View File
@@ -0,0 +1,99 @@
"""Проверки адресованной прошивки и защиты данных без подключения реального MCU.""" # Подмена касается только файлов снимков, production-план остаётся настоящим.
from dataclasses import replace # Позволяет воспроизводить неверные настройки и адрес BIN независимо от формы.
from pathlib import Path # Тесты создают все входы только во временном каталоге pytest.
import struct # Создаёт корректные Cortex-M3-векторы и проверяет настоящий формат manifest UKSVEP.
import zlib # Независимо вычисляет ожидаемый CRC32 приложения.
import pytest # Параметры покрывают ошибки до первой аппаратной записи.
from firmware_image.core import HexSegment, parse_intel_hex # Канонический parser проверяет сформированный снимок для Commander.
from set_devices.firmware import FirmwareImage # Испытывает тот же тип образа, который передаёт SETGUI.
from set_devices.stm32_jtag import JLinkOptions, FLASH_BASE, UKS_APP, UKS_PARAMS, UKS_META, prepare_image, encode_hex, programming_script, start_script, verify_readback # Проверяет публичную переносимую реализацию templates.
from set_devices import stm32_jtag as module # Позволяет изолировать поиск установленного программатора от настоящего PATH Windows.
def app_image(tmp_path, extra=()): # Создаёт небольшой корректный app HEX и необязательные отдельные диапазоны.
data = struct.pack('<II', 0x2000C000, UKS_APP + 9) + bytes(range(64)) # MSP соответствует 48-КиБ SRAM; Thumb Reset_Handler находится внутри payload.
path = tmp_path / 'app.hex' # Исходный файл не содержит служебного manifest, как обычный postbuild приложения.
path.write_text(encode_hex([HexSegment(UKS_APP, data), *extra]), encoding='utf-8') # Реальный Intel HEX проходит штатную загрузку FirmwareImage.
return FirmwareImage.load(path) # Позволяет проверять также сохранность выбора и отказ при изменении файла.
def test_app_only_gets_production_manifest_and_sparse_snapshot(tmp_path): # JTAG app-only обязан остаться запускаемым постоянным загрузчиком.
image, options = app_image(tmp_path), JLinkOptions() # Использует проверенный профиль UKSVEP STM32F103RC.
plan = prepare_image(image, options) # Production-план автоматически добавляет запись активации приложения.
assert [(part.start, len(part.data)) for part in plan] == [(UKS_APP, 72), (UKS_META, 24)] # Параметры и разрыв не превращаются в данные для записи.
magic, size, crc, nsize, ncrc, product = struct.unpack('<6I', plan[-1].data) # Формат должен читаться настоящим boot_port.c без адаптации.
assert (magic, size, crc, nsize, ncrc, product) == (0x554B5356, 72, zlib.crc32(image.data), ~72 & 0xFFFFFFFF, ~zlib.crc32(image.data) & 0xFFFFFFFF, 0x5606) # Проверяет взаимные инверсии и аппаратный Product UKSVEP.
assert tuple(parse_intel_hex(encode_hex(plan))) == plan # Снимок Commander сохраняет точные адреса и байты обоих диапазонов.
assert not any(part.start <= UKS_PARAMS <= part.end for part in plan) # Страница настроек отсутствует в адресованных data-записях.
def test_factory_image_with_boot_and_manifest_is_not_flattened(tmp_path): # Полный install HEX не должен стереть промежуточную страницу параметров.
image = app_image(tmp_path, [HexSegment(FLASH_BASE, b'boot' * 64)]) # Адрес загрузчика отделён от приложения разрывом.
plan = prepare_image(image, JLinkOptions()) # Сначала получает правильный manifest для готового установочного комплекта.
image.path.write_text(encode_hex(plan), encoding='utf-8') # Моделирует опубликованный boot+app+manifest HEX.
assert prepare_image(FirmwareImage.load(image.path), JLinkOptions()) == plan # Повторная подготовка не добавляет FF и не меняет готовый manifest.
@pytest.mark.parametrize('address', [UKS_PARAMS - 1, UKS_PARAMS, UKS_PARAMS + 2047]) # Касание любого края защищённой страницы запрещено.
def test_parameter_page_overlap_is_rejected(tmp_path, address): # Ошибка выявляется до обращения к J-Link.
image = app_image(tmp_path, [HexSegment(address, b'xx')]) # Два байта проверяют также вход в страницу с предыдущего адреса.
with pytest.raises(ValueError, match='страницу параметров'): prepare_image(image, JLinkOptions()) # Полный дамп нельзя случайно записать вместо установочного разреженного HEX.
def test_stale_image_and_relocated_hex_are_rejected(tmp_path): # Выбор и подтверждение должны относиться к тем же байтам и адресам.
image = app_image(tmp_path) # Сохраняет исходный снимок формы до вмешательства в файл.
with pytest.raises(ValueError, match='адрес HEX'): prepare_image(replace(image, base_address=UKS_APP + 4), JLinkOptions()) # Явный адрес HEX нельзя переопределять как адрес BIN.
image.path.write_text(encode_hex([HexSegment(UKS_APP, image.data + b'changed')]), encoding='utf-8') # Моделирует пересборку файла после выбора пользователем.
with pytest.raises(ValueError, match='изменён'): prepare_image(image, JLinkOptions()) # До повторного выбора аппаратное программирование запрещено.
def test_bin_needs_address_and_accepts_another_stm32(tmp_path): # Общий режим не применяет специальные адреса и manifest UKSVEP.
path = tmp_path / 'raw.bin' # BIN не содержит собственной карты памяти.
path.write_bytes(bytes(range(64))) # Для обычного MCU не навязывается структура приложения UKSVEP.
image, options = FirmwareImage.load(path), JLinkOptions(device='STM32G474VE', flash_kib=512, interface='SWD', uksvep=False) # Использует другую физическую карту STM32.
with pytest.raises(ValueError, match='объём Flash'): prepare_image(image, options) # Нулевой адрес BIN не означает RAM или неявное начало Flash.
assert prepare_image(replace(image, base_address=FLASH_BASE), options) == (HexSegment(FLASH_BASE, image.data),) # Явный корректный адрес создаёт ровно один диапазон без постороннего manifest.
@pytest.mark.parametrize('changes', [{'device': 'STM32F103RC\ng'}, {'serial': '123 -erase'}, {'interface': 'UART'}, {'speed_khz': 0}, {'flash_kib': 0}, {'device': 'STM32F407VE'}]) # Проверяет интерфейс, размеры, профиль и внедрение дополнительных команд.
def test_invalid_target_is_rejected(changes): # Поля будущего command file валидируются единым контрактом.
with pytest.raises(ValueError): replace(JLinkOptions(), **changes).arguments() # Ошибка не требует создания процесса и не открывает программатор.
def test_readback_rejects_partial_write_and_lost_parameters(tmp_path): # Ложный нулевой exit code не должен привести к запуску испорченного MCU.
options = JLinkOptions() # Сохранность параметров проверяется для конкретного UKSVEP-профиля.
plan = prepare_image(app_image(tmp_path), options) # Использует реальный production-план с manifest.
before = bytes((index * 7) & 255 for index in range(256 * 1024)) # Полный backup содержит не-FF настройки, чтобы их потеря была заметной.
after = bytearray(before) # Моделирует неизменённые области между реально записанными диапазонами.
for part in plan: after[part.start - FLASH_BASE:part.start - FLASH_BASE + len(part.data)] = part.data # Применяет только адресованные данные плана.
verify_readback(plan, options, before, after) # Полная корректная запись разрешает отдельный этап reset/run.
after[UKS_PARAMS - FLASH_BASE] ^= 1 # Воспроизводит потерю одного бита существующих параметров.
with pytest.raises(ValueError, match='параметров'): verify_readback(plan, options, before, after) # Сохранность protected page проверяется по аппаратным снимкам.
after[UKS_PARAMS - FLASH_BASE] ^= 1 # Восстанавливает параметры для отдельной проверки ошибки программирования приложения.
after[UKS_APP - FLASH_BASE + 8] ^= 1 # Повреждает один реально записываемый байт приложения.
with pytest.raises(ValueError, match='Ошибка проверки'): verify_readback(plan, options, before, after) # Запуск запрещён даже при совпавшей длине обоих SaveBin.
with pytest.raises(ValueError, match='полный backup'): verify_readback(plan, options, before, b'') # Преждевременный выход Commander с кодом ноль не считается успехом.
def test_scripts_separate_programming_from_reset_run(tmp_path): # Архитектура не должна запускать ядро до проверки на хосте.
options = JLinkOptions(serial='603000869') # Явный серийный номер должен сохраняться отдельным аргументом.
program, run = programming_script(tmp_path / 'folder with spaces', options), start_script(options) # Пути с пробелами должны быть корректно заключены в кавычки.
assert program.index('before.bin') < program.index('loadfile') < program.index('after.bin') # Backup выполняется до первой записи, readback — после неё.
assert '\ng\n' not in program and '\ng\n' in run and 'loadfile' not in run # Отдельный проверенный этап содержит только reset/служебные регистры/run.
assert 'erase' not in program.lower() # Не вводит массовое стирание через новую общую реализацию.
assert options.arguments()[:2] == ['-USB', '603000869'] # Выбранный J-Link не заменяется другим подключённым адаптером.
assert '-JTAGConf' in options.arguments() and '-1,-1' in options.arguments() # Batch не должен остановиться на интерактивном запросе цепочки JTAG.
def test_hex_encoder_handles_unaligned_64k_boundary(): # Граничный адрес внутри сегмента не должен переполнить 16-битное поле записи.
segments = (HexSegment(0x0800FFF9, bytes(range(40))),) # Невыровненный старт пересекает окно 64 КиБ внутри первой порции.
assert tuple(parse_intel_hex(encode_hex(segments))) == segments # Независимый C-parser подтверждает корректную смену Extended Linear Address.
def test_autodetection_does_not_select_java_jlink(monkeypatch): # Windows PATH может раньше SEGGER содержать одноимённый инструмент JDK.
java = 'C:/jdk/bin/JLink.exe' # Имя совпадает без учёта регистра с программатором SEGGER.
monkeypatch.setattr(module.shutil, 'which', lambda name: java) # Установленный в системе J-Link не участвует в этой проверке.
monkeypatch.setattr(Path, 'is_file', lambda path: path.as_posix() in (java, 'C:/jdk/bin/java.exe')) # Моделирует только bin JDK и отсутствие Keil/SEGGER.
assert module.find_jlink() == '' # Общая форма должна запросить настоящий executable вместо запуска Java.
monkeypatch.setattr(Path, 'is_file', lambda path: path.as_posix() in (java, 'C:/jdk/bin/java.exe', 'C:/Keil_v5_41/ARM/Segger/JLink.exe')) # Добавляет именно установленный SEGGER, не меняя неоднозначный PATH.
assert module.find_jlink().replace('\\', '/') == 'C:/Keil_v5_41/ARM/Segger/JLink.exe' # Проверенный каталог Keil получает приоритет над JDK.
@@ -1,6 +1,41 @@
{
"product": "MY_CONTROLLER",
"target": "TMS320F28335",
"version": [0, 1, 0],
"increment_version": true
"version": [
0,
1,
0
],
"increment_version": true,
"flashing": {
"default_loader": "f28335-eeprom",
"profiles": {
"f28335-eeprom": {
"baudrate": 115200,
"controller": 16,
"timeout_ms": 500
},
"f28335-flash-rs": {
"baudrate": 115200,
"controller": 16,
"timeout_ms": 500
},
"protocan-can": {
"bitrate": 250000,
"device_type": 1,
"node_id": 8,
"session_id": 1,
"product_type": 10291,
"hardware_min": 1,
"hardware_max": 1,
"memory": "eeprom"
},
"f28335-sci": {
"baudrate": 115200,
"oscillator_hz": 30000000,
"enter_monitor": false,
"timeout_ms": 3000
}
}
}
}
+4 -2
View File
@@ -19,11 +19,13 @@ if "%~6"=="" goto usage
if not "%~8"=="" goto usage
if "%~7"=="" goto identity_local
rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI.
python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" --export "%~f7"
rem Общий runtime автоматически проверяет и подготавливает Python для шага IDE.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" --export "%~f7"
exit /b %errorlevel%
:identity_local
rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI.
python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6"
rem Общий runtime автоматически проверяет и подготавливает Python для шага IDE.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6"
exit /b %errorlevel%
:usage
echo Usage: postcompile.bat --identity "compiler-root" "build-dir" "artifact-name" "generated-metadata.json" "output-dir" ["export-dir"] 1>&2
+2 -1
View File
@@ -24,7 +24,8 @@ if not exist "%~f3" (
echo [firmware-build] Edit product, target and version in this file, then rebuild.
)
rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI.
python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" prepare --project "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4"
rem Общий runtime автоматически проверяет и подготавливает Python для шага IDE.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" prepare --project "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4"
exit /b %errorlevel%
:usage
echo Usage: precompile.bat --identity "project-root" "firmware_identity.json" "generated-dir" 1>&2
@@ -2,5 +2,22 @@
"product": "MY_CONTROLLER",
"target": "STM32F407",
"version": [0, 1, 0],
"increment_version": true
"increment_version": true,
"flashing": {
"default_loader": "set-can",
"profiles": {
"set-can": {
"bitrate": 250000,
"node_id": 13,
"product_type": 358,
"hardware_min": 1,
"hardware_max": 1,
"base_address": 134230016
},
"set-rs485": {
"baudrate": 115200,
"base_address": 134230016
}
}
}
}
+8 -4
View File
@@ -18,15 +18,19 @@ if /i not "%~x2"==".hex" if /i not "%~x2"==".bin" (
echo [firmware-build] Expected a ready HEX or BIN image: "%~2" 1>&2
exit /b 2
)
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" seal --image "%~f2" --metadata "%~f3"
rem Общий runtime автоматически проверяет и подготавливает Python для шага IDE.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" seal --image "%~f2" --metadata "%~f3"
if errorlevel 1 exit /b %errorlevel%
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" verify --image "%~f2" --manifest "%~f2.manifest.json"
rem Общий runtime автоматически проверяет и подготавливает Python для шага IDE.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" verify --image "%~f2" --manifest "%~f2.manifest.json"
if errorlevel 1 exit /b %errorlevel%
if "%~4"=="" goto archive_local
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3" --export "%~f4"
rem Общий runtime автоматически проверяет и подготавливает Python для шага IDE.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3" --export "%~f4"
goto archive_done
:archive_local
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3"
rem Общий runtime автоматически проверяет и подготавливает Python для шага IDE.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3"
:archive_done
if errorlevel 1 exit /b %errorlevel%
echo [firmware-build] Image: "%~f2"
+2 -1
View File
@@ -16,7 +16,8 @@ if "%~2"=="" goto usage
if "%~3"=="" goto usage
if "%~4"=="" goto usage
if not "%~5"=="" goto usage
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" prepare --repository "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4"
rem Общий runtime автоматически проверяет и подготавливает Python для шага IDE.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" prepare --repository "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4"
exit /b %errorlevel%
:usage
echo Usage: precompile.bat --identity "project-root" "firmware_identity.json" "generated-dir" 1>&2
+87
View File
@@ -0,0 +1,87 @@
[CmdletBinding(PositionalBinding = $false)] # Именованные служебные параметры не должны поглощать позиционный путь запускаемого Python-скрипта.
param( # Единая точка входа CCS/Keil работает без зависимости от python.exe в PATH.
[string]$ResolveFile, # При настройке IDE записывает точный путь проверенного интерпретатора в UTF-8.
[switch]$CheckOnly, # Проверка окружения не скачивает runtime и не запускает проектный скрипт.
[Parameter(Position = 0, ValueFromRemainingArguments = $true)][string[]]$PythonArguments # Путь скрипта и все оставшиеся аргументы передаются Python без позиционной привязки к ResolveFile.
) # Завершает описание публичных параметров обвязки.
$ErrorActionPreference = 'Stop' # Любая ошибка подготовки возвращается IDE как отказ шага сборки.
$env:PYTHONUTF8 = '1' # Python 3.8+ передаёт пути и диагностику в UTF-8 независимо от локальной кодовой страницы Windows.
[Console]::OutputEncoding = New-Object Text.UTF8Encoding($false) # Windows PowerShell 5 должен читать вывод Python в той же кодировке.
$OutputEncoding = [Console]::OutputEncoding # Согласует кодировку дочерних native-процессов с JSON и сообщениями SETGUI.
function Test-FirmwarePython([string]$Candidate) { # Проверяет настоящий Python вместо одноимённого псевдонима Microsoft Store.
if (-not $Candidate -or -not (Test-Path -LiteralPath $Candidate -PathType Leaf) -or $Candidate -like '*WindowsApps*') { return $null } # Пропускает отсутствующие файлы и псевдонимы магазина без открытия окна установки.
try { # Неподходящий кандидат не мешает проверить остальные установленные интерпретаторы.
$result = & $Candidate -c 'import sys,json,hashlib,struct,tempfile,ctypes,xml.etree.ElementTree;sys.exit(1) if sys.version_info<(3,8) else print(sys.executable)' 2>$null # Общим инструментам нужны Python 3.8+ и только стандартная библиотека.
if ($LASTEXITCODE -eq 0 -and $result) { return [string]($result | Select-Object -Last 1) } # Возвращает фактический exe, а не имя команды или путь launcher.
} catch {} # Ошибка запуска одного кандидата оставляет поиск остальных доступным.
return $null # Отсутствие пригодного runtime обрабатывается общей установкой ниже.
} # Завершает проверку одного интерпретатора.
function Find-FirmwarePython { # Ищет готовый runtime до любых сетевых действий.
if ($env:FIRMWARE_PYTHON) { # Явный выбор администратора проекта имеет приоритет над автоматическим поиском.
$chosen = Test-FirmwarePython $env:FIRMWARE_PYTHON # Проверяет указанный абсолютный путь тем же набором импортов.
if (-not $chosen) { throw 'FIRMWARE_PYTHON должен указывать на работающий Python 3.8+.' } # Неверное явное значение не подменяется другой версией незаметно.
return $chosen # Оба шага IDE используют выбранный интерпретатор.
} # После явной настройки остальные кандидаты не нужны.
$cache = Join-Path $env:LOCALAPPDATA 'SET/FirmwareBuild' # Локальный runtime принадлежит пользователю и не требует администратора.
foreach ($relative in @('python-3.13.12-amd64/python.exe', 'python-3.8.10-win32/python.exe')) { # Порядок предпочитает современный runtime, сохраняя совместимость с Windows 7.
$chosen = Test-FirmwarePython (Join-Path $cache $relative) # Проверяет ранее подготовленную локальную копию.
if ($chosen) { return $chosen } # Повторные сборки обходятся без скачивания и установки.
} # Завершает поиск локального управляемого runtime.
foreach ($command in @('python', 'python3')) { # Установленный Python можно использовать без изменения пользовательского PATH.
$candidate = Get-Command $command -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1 # Получает полный путь исполняемого файла команды.
if ($candidate) { $chosen = Test-FirmwarePython $candidate.Source; if ($chosen) { return $chosen } } # Не принимает неподходящую версию за готовое окружение.
} # После PATH проверяет зарегистрированные установки без требования перезапуска IDE.
foreach ($registry in @('HKCU:/Software/Python/PythonCore', 'HKLM:/Software/Python/PythonCore', 'HKLM:/Software/WOW6432Node/Python/PythonCore')) { # PEP 514 учитывает установки текущего пользователя, x64 и x86.
foreach ($version in @(Get-ChildItem -LiteralPath $registry -ErrorAction SilentlyContinue | Sort-Object PSChildName -Descending)) { # Перебирает только зарегистрированные версии Python.
$key = Get-Item -LiteralPath ($version.PSPath + '/InstallPath') -ErrorAction SilentlyContinue # Реестр хранит корневой каталог независимой установки.
if ($key) { $chosen = Test-FirmwarePython (Join-Path ([string]$key.GetValue('')) 'python.exe'); if ($chosen) { return $chosen } } # Возвращает первую реально работающую версию 3.8+.
} # Завершает перебор версий одного раздела реестра.
} # Ни один существующий runtime не оказался пригодным.
return $null # Вызывающий подготовит переносимый runtime, если проверка допускает установку.
} # Завершает поиск установленного Python.
try { # Все ошибки возвращаются одним ненулевым кодом, понятным CCS/Keil.
$pythonExe = Find-FirmwarePython # Использует готовую установку без изменения окружения машины.
if (-not $pythonExe -and $CheckOnly) { throw 'Python 3.8+ не найден; примените автоматическую настройку проекта для подготовки runtime.' } # Режим диагностики не устанавливает ПО.
if (-not $pythonExe) { # Установка нужна только компьютеру без подходящего Python.
$legacy = [Environment]::OSVersion.Version -lt [Version]'6.2' -or -not [Environment]::Is64BitOperatingSystem # Windows 7 и x86 используют последнюю совместимую бинарную ветку 3.8.
$version = if ($legacy) { '3.8.10' } else { '3.13.12' } # Зафиксированные версии делают подготовку воспроизводимой.
$arch = if ($legacy) { 'win32' } else { 'amd64' } # Разрядность runtime не зависит от разрядности установленной IDE.
$expected = if ($legacy) { '760dc79bcb434ee80b1001a30bb6f798287881851bac6d8137867894d40ef1fc' } else { '76f238f606250c87c6beac75dccd35ee99070a13490555936abb6cb64ecce3d0' } # SHA-256 официальных embeddable ZIP с python.org проверен при подготовке обвязки.
$cache = Join-Path $env:LOCALAPPDATA 'SET/FirmwareBuild' # Runtime хранится отдельно от исходников и сохраняется после Clean.
$destination = Join-Path $cache ('python-' + $version + '-' + $arch) # Каждый вариант получает независимый каталог пользователя.
New-Item -ItemType Directory -Force -Path $cache | Out-Null # Создаёт только собственный каталог инструментов сборки.
$temporary = Join-Path $cache ('download-' + [Guid]::NewGuid().ToString('N')) # Уникальный staging исключает частично установленный runtime.
New-Item -ItemType Directory -Path $temporary | Out-Null # Распаковка выполняется вне постоянного каталога.
try { # Ошибка сети или распаковки не должна повредить ранее установленный Python.
$archive = Join-Path $temporary 'python.zip' # Собственный архив удаляется вместе со staging.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # Загрузка использует TLS 1.2 также на Windows 7 с обновлённой .NET.
$client = New-Object Net.WebClient # WebClient доступен штатной Windows PowerShell без внешних модулей.
try { $client.DownloadFile(('https://www.python.org/ftp/python/' + $version + '/python-' + $version + '-embed-' + $arch + '.zip'), $archive) } finally { $client.Dispose() } # Скачивает строго официальный зафиксированный пакет Python.
$sha = [Security.Cryptography.SHA256]::Create() # Проверяет полные байты архива до запуска любого файла из него.
try { $actual = ([BitConverter]::ToString($sha.ComputeHash([IO.File]::ReadAllBytes($archive)))).Replace('-', '').ToLowerInvariant() } finally { $sha.Dispose() } # Нормализует SHA-256 для точного сравнения с закреплённым значением.
if ($actual -ne $expected) { throw 'SHA-256 архива Python не совпал с ожидаемым значением.' } # Неподтверждённый архив никогда не распаковывается.
Add-Type -AssemblyName System.IO.Compression.FileSystem # Использует штатную .NET 4.5+ для ZIP без зависимости от Python.
$unpacked = Join-Path $temporary 'runtime' # Отделяет исполняемые файлы от скачанного ZIP.
[IO.Compression.ZipFile]::ExtractToDirectory($archive, $unpacked) # Распаковывает проверенный официальный ZIP во временный каталог.
$checked = Test-FirmwarePython (Join-Path $unpacked 'python.exe') # До публикации проверяет версию и необходимые модули runtime.
if (-not $checked) { throw 'Подготовленный Python не запускается; проверьте Universal C Runtime Windows.' } # Отсутствующая системная зависимость не выдаётся за успех установки.
if (Test-Path -LiteralPath $destination) { throw ('Каталог runtime занят непригодной установкой: ' + $destination) } # Не удаляет существующие пользовательские файлы автоматически.
Move-Item -LiteralPath $unpacked -Destination $destination # Атомарно публикует только полностью проверенную локальную установку.
$pythonExe = Join-Path $destination 'python.exe' # Все последующие вызовы используют точный путь установленного runtime.
} finally { # Очищает только собственный staging после проверки его абсолютного расположения.
$resolved = [IO.Path]::GetFullPath($temporary) # Нормализует путь перед рекурсивным удалением на Windows.
if ($resolved.StartsWith([IO.Path]::GetFullPath($cache) + [IO.Path]::DirectorySeparatorChar, [StringComparison]::OrdinalIgnoreCase)) { Remove-Item -LiteralPath $resolved -Recurse -Force -ErrorAction SilentlyContinue } # Удаляет исключительно созданную текущим вызовом папку внутри собственного кэша.
} # Завершает подготовку одного переносимого runtime.
} # Готовый Python найден или успешно подготовлен.
if ($ResolveFile) { [IO.File]::WriteAllText([IO.Path]::GetFullPath($ResolveFile), $pythonExe, (New-Object Text.UTF8Encoding($false))) } # GUI получает точный путь без разбора локализованного вывода консоли.
if ($PythonArguments.Count) { # Непустые аргументы означают запуск шага сборки после подготовки зависимостей.
$bootstrap = 'import os,runpy,sys;p=sys.argv[1];sys.argv=sys.argv[1:];sys.path.insert(0,os.path.dirname(os.path.abspath(p)));runpy.run_path(p,run_name=''__main__'')' # Одинарные Python-кавычки сохраняются при передаче через Windows PowerShell 5 и изолированный runtime.
& $pythonExe -c $bootstrap @PythonArguments # Передаёт исходные аргументы отдельными элементами без повторного разбора командной оболочкой.
exit $LASTEXITCODE # Ошибка Python останавливает prebuild/postbuild с исходным кодом завершения.
} # В режиме настройки достаточно успешной подготовки runtime.
Write-Output $pythonExe # Терминал показывает фактически выбранный интерпретатор.
exit 0 # Успех означает работающий Python с проверенной стандартной библиотекой.
} catch { # Единая диагностика доступна как в IDE, так и в окне настройки SETGUI.
[Console]::Error.WriteLine('[firmware-build] ' + $_.Exception.Message) # Показывает конкретную причину отказа без скрытого перехода к другой цепочке.
exit 2 # Ненулевой код запрещает дальнейшие стадии неподготовленной сборки.
} # Завершает общий запуск runtime и инструментов прошивки.