Перенести паспорт и проверку прошивки из 167 в 166

Добавить неизменяемое окно firmware-info v1 0x1200 из 12 слов для FC03 и служебного CAN, запретить запись. Сохранить текущую технологическую карту и логгер v4 с записью 12 слов.

Подключить общий генератор версии 1.0.0 для Balsam-166-PM35, prebuild CCS и локальный выпуск BIN с manifest и SHA-256. Проверять соответствие встроенных слов манифесту до публикации; старый OUT без паспорта отклонять.

Проверка: реальные обработчики RS/CAN и CRC под GCC O3, прежний тест логгера, анализатор установленного SetGUI, полная сборка TI C2000 22.6.0.LTS. BIN 30248 байт при границе параметров 32508 байт. Аппаратная проверка требует установки нового образа и перезапуска платы.
This commit is contained in:
2026-10-16 12:05:44 +03:00
parent 001ade4b20
commit fe95dbff3d
15 changed files with 327 additions and 61 deletions

View File

@@ -0,0 +1,10 @@
#ifndef BALSAM_FIRMWARE_INFO_H /* Исключает повторное объявление адаптера паспорта при совместном подключении RS и CAN. */
#define BALSAM_FIRMWARE_INFO_H /* Обозначает единственный интерфейс firmware-info v1 приложения 166. */
#include "firmware_info_static.h" /* Использует тот же статический 12-словный контракт общей библиотеки, который подключён в 167. */
#define FIRMWARE_INFO_ADDRESS 0x1200U /* Адрес firmware-info v1 из протокола 167 и SetGUI; не пересекается с логгером 0x1000..0x117B. */
#define FIRMWARE_INFO_END (FIRMWARE_INFO_ADDRESS + FIRMWARE_INFO_STATIC_COUNT) /* Исключающая верхняя граница 0x120C охватывает ровно двенадцать 16-битных регистров. */
static unsigned int firmware_info_range(unsigned int address, unsigned int count) /* Проверяет только чтение внутри паспорта без переполнения 16-битной суммы адреса и длины. */
{ /* Вычитание выполняется после проверки адреса, поэтому не переполняется на C28x. */
return address >= FIRMWARE_INFO_ADDRESS && address < FIRMWARE_INFO_END && count > 0U && count <= FIRMWARE_INFO_END - address; /* Разрешает полное и частичное чтение, запрещая нулевую длину и пересечение границы паспорта. */
} /* Не меняет правила остальных технологических и служебных адресов. */
#endif /* Завершает адаптер, не добавляя изменяемое состояние или параметры EEPROM. */

View File

@@ -10,6 +10,7 @@
#ifndef UMP_CAN_PROTOCOL_H
#define UMP_CAN_PROTOCOL_H
#include "log_to_mem.h"
#include "firmware_info.h" /* Использует то же окно firmware-info v1, что FC03, сохраняя остальной служебный CAN-протокол 166. */
#define UMP_CAN_MARKER 0x1F00U
#define UMP_CAN_MAX_WORDS 124U
@@ -21,6 +22,10 @@ static unsigned int ump_can_prepare(unsigned int function, unsigned int address,
unsigned int i;
*error = 0;
if (function == 3) {
if (firmware_info_range(address, value)) { /* Признаёт только чтение внутри нового двенадцатисловного паспорта. */
for (i = 0; i < value; ++i) reply[i] = firmware_info_static_read(address - FIRMWARE_INFO_ADDRESS + i); /* Общий статический упаковщик выдаёт те же слова, что RS, без чтения архива логгера. */
return value; /* Передаёт паспорт существующему механизму фрагментов CAN без изменения mailbox, ID и повторов. */
} /* Все остальные запросы продолжают проходить прежнюю проверку окон логгера v4. */
if (!((address == UMP_LOG_BASE && value == 16) ||
/* Разрешает от одной до десяти целых 12-словных записей v4 в пределах 120 слов полезного окна CAN. */
(address == UMP_LOG_DATA && value >= UMP_LOG_WORDS && value <= 120U && value % UMP_LOG_WORDS == 0U) ||

View File

@@ -24,6 +24,8 @@
#include "caliber.h"
#include "log_to_mem.h"
#include "firmware_info.h" /* Подключает адрес и границы неизменяемого паспорта 0x1200, совместимого с проверкой прошивки в 167. */
#include "../src/firmware_info_static.c" /* Собирает общие 12 слов firmware-info через include-путь firmware-info/include; как в 167, копии упаковщика в проекте нет. */
#include "DAC.h"
int modbus[ANSWER_LEN+1];
@@ -223,7 +225,8 @@ void ReceiveCommandModbus3(RS_DATA *rs_arr)
Length_MB <= UMP_LOG_BASE+16-Address_MB) ||
(Address_MB >= UMP_LOG_DATA && Address_MB < UMP_LOG_DATA+120 &&
Length_MB <= UMP_LOG_DATA+120-Address_MB) ||
(Address_MB == UMP_LOG_LIVE && Length_MB == 124))) {
(Address_MB == UMP_LOG_LIVE && Length_MB == 124) || /* Сохраняет точное прежнее 124-словное онлайн-окно логгера. */
firmware_info_range(Address_MB, Length_MB))) { /* Дополнительно разрешает только полное или частичное чтение двенадцати регистров паспорта. */
modbus_exception(rs_arr, CMD_MODBUS_3, 2); return;
}
@@ -237,15 +240,16 @@ void ReceiveCommandModbus3(RS_DATA *rs_arr)
/* Защищаем целостность одного снимка/блока; передача UART идёт уже
* с разрешёнными прерываниями и не задерживает измерение/управление. */
if (Address_MB >= UMP_LOG_BASE) saved = __disable_interrupts();
if (Address_MB >= UMP_LOG_BASE && Address_MB < FIRMWARE_INFO_ADDRESS) saved = __disable_interrupts(); /* Неизменяемый паспорт не требует блокировки IRQ; целостность прежних снимков логгера сохраняется. */
for (i=0;i<Length_MB;i++)
{
value = Address_MB >= UMP_LOG_BASE ? ump_log_read(Address_MB+i) : modbus[Address_MB+i];
if (Address_MB >= FIRMWARE_INFO_ADDRESS) value = firmware_info_static_read(Address_MB - FIRMWARE_INFO_ADDRESS + i); /* После проверки диапазона читает только соответствующее слово статического паспорта, без обращения к логгеру. */
else value = Address_MB >= UMP_LOG_BASE ? ump_log_read(Address_MB+i) : (unsigned int)modbus[Address_MB+i]; /* Сохраняет прежнюю маршрутизацию и беззнаковое представление 16-битных слов технологической карты и логгера v4. */
rs_arr->buffer[3+i*2] = (value >> 8) & 0xFF;
rs_arr->buffer[3+i*2+1] = value & 0xFF;
}
if (Address_MB >= UMP_LOG_BASE) __restore_interrupts(saved);
if (Address_MB >= UMP_LOG_BASE && Address_MB < FIRMWARE_INFO_ADDRESS) __restore_interrupts(saved); /* Возвращает сохранённую маску только для действительно защищённого снимка логгера. */
crc = 0xffff;
crc = get_crc_16(crc, rs_arr->buffer, Length_MB*2+3);
@@ -283,6 +287,7 @@ void ReceiveCommandModbus6(RS_DATA *rs_arr)
// получили слово данных
Data_MB = (rs_arr->RS_Header[4] << 8) | rs_arr->RS_Header[5];
if (Address_MB >= FIRMWARE_INFO_ADDRESS && Address_MB < FIRMWARE_INFO_END) { modbus_exception(rs_arr, CMD_MODBUS_6, 2); return; } /* Как в контракте 167, запись в паспорт запрещена с Illegal Data Address и не достигает параметров или логгера. */
if (Address_MB >= UMP_LOG_BASE) {
if (!ump_log_write(Address_MB, Data_MB)) {

File diff suppressed because one or more lines are too long

View File

@@ -16,14 +16,47 @@
Файл `28335_RAM_lnk.cmd`, как и в 167, исключён из сборки.
Результаты сборки находятся в `166/Bin`: `UKSSTMS320F28335.out`,
`UKSSTMS320F28335.hex` (Intel HEX) и `UKSSTMS320F28335_<commit>.bin`,
где `<commit>` — короткий хеш текущего коммита Git (не менее 8 символов).
BIN создаётся скриптом `build_bin.bat`; Git должен быть доступен в `PATH`
процесса CCS. Если определить коммит не удалось, сборка завершается ошибкой.
При наличии незакоммиченных правок хеш обозначает HEAD и не описывает эти правки.
Post-build использует имеющийся `166/Bin/hex2000.exe` с параметрами
`-boot -sci8`, как в 167. Карта линковки находится в корне 166, карта
преобразования загрузочного образа — в `166/Bin`.
`UKSSTMS320F28335.hex` (Intel HEX) и `UKSSTMS320F28335_<build_id>.bin`.
Рядом с BIN создаются `<имя>.bin.manifest.json` и `<имя>.bin.sha256`.
Build ID содержит восемь символов: восемь цифр ревизии либо семь цифр и `+`
при изменённых исходниках или общей библиотеке. При недоступном Git общий
контракт 167 использует `NOGIT000`; для выпуска следует проверить ревизии
`source` и `templates` в манифесте. Время паспорта хранится в UTC.
Prebuild `prepare_identity.py` использует тот же генератор firmware-info,
что 167, но product равен `Balsam-166-PM35`. Единственный источник версии —
`firmware_version.h`; первый выпуск с паспортом имеет версию **1.0.0**.
Обязательный `generated/firmware_identity.h` создаётся до компиляции.
Postbuild `build_identity.py` использует hex2000 из выбранного компилятора
CCS, общий манифест и SHA-256, затем независимо проверяет наличие ровно
одного соответствующего 12-словного паспорта в SCI8. Старый OUT без паспорта
или BIN с чужими полями не публикуется. Превышение границы параметров EEPROM
**32508 байт** также останавливает выпуск. Архивы сохраняются локально в
`UKSSTMS320F28335/Debug/firmware`; записи в репозиторий GUI нет.
`build_bin.bat` сохраняет ручную точку входа для уже скомпилированного OUT,
но теперь также требует его совпадения с паспортом соответствующего prebuild.
Простая конверсия старой прошивки не может создать ей достоверную версию.
Карта линковки остаётся в корне 166, карта конверсии — в `166/Bin`.
## Проверка прошивки в SetGUI
Из 167 точечно перенесён контракт firmware-info v1: FC03, адрес **0x1200**,
**12 регистров**, версия, дата UTC и Build ID. Чтение доступно также через
существующий служебный CAN; запись в паспорт запрещена. Технологическая
карта, логгер v4 с записью 12 слов, управление и CAN ID не меняются.
В GUI выберите модель **«Бальзам-166 · ПМ35»**, новый BIN и оставьте рядом
его `.bin.manifest.json`. После записи EEPROM перезапустите плату и нажмите
чтение версии: продолжающая работать старая прошивка ещё не имеет окна
0x1200 и отвечает `Modbus 2`. Надпись CRC OK при записи подтверждает запись
EEPROM, а версия начнёт читаться после запуска нового образа. Сравнение
версий и SHA-256 манифеста не является измерением SHA всей памяти платы.
`python tests/firmware_identity/run_test.py` проверяет реальные обработчики
RS/CAN, все слова паспорта, CRC, частичные чтения, границы, запрет записи,
сохранность старой карты и логгера. Отдельно проверяются устаревшие и
повреждённые SCI8, предел EEPROM и действующий анализатор манифеста SetGUI.
Проверено: импорт и полная сборка **Debug** в CCS **12.7.1** с компилятором
**22.6.1.LTS**, включая генерацию HEX/BIN, завершились без ошибок.
@@ -31,5 +64,12 @@ Post-build использует имеющийся `166/Bin/hex2000.exe` с па
(преобразование знака, выход результата за диапазон и отсутствие перевода
строки в конце файла). Проверка на устройстве не выполнялась.
Перенос паспорта дополнительно проверен полной пересборкой и линковкой
TI C2000 **22.6.0.LTS** и host-тестами под GCC с оптимизацией O3. Проверенный
SCI8 BIN занимает **30248 байт**, запас до параметров EEPROM — **2260 байт**.
Совпадение встроенного паспорта, манифеста и результата штатного анализатора
SetGUI проверено программно; обмен новой прошивки с физической платой
требует её установки и перезапуска.
Конфигурация **Release** перенесена из 167 без изменений и не проверялась;
для сборки прошивки используйте **Debug**.

View File

@@ -0,0 +1,67 @@
"""Выпускает BIN 166 с проверяемым паспортом и SHA-256 по той же схеме, что 167.""" # Генерация версии выполняется отдельно до компиляции.
import argparse # Разбирает пути текущей конфигурации CCS и компилятора TI.
from datetime import datetime # Преобразует время UTC паспорта в те же шесть компонентов, что firmware-info v1.
import json # Читает именно паспорт завершившейся компиляции.
from pathlib import Path # Привязывает настройки и результаты к текущему проекту.
import shutil # Находит компилятор и копирует только полностью проверенный выпуск.
import struct # Проверяет 16-битные слова загрузочного SCI8 без исполнения кода прошивки.
import sys # Подключает общий postbuild вместо копии конвертера из 167.
import tempfile # Не публикует результаты конверсии, пока встроенный паспорт не прошёл проверку.
ROOT = Path(__file__).resolve().parents[1] # Корень 166 содержит существующие Source и Bin.
PROJECT = Path(__file__).resolve().parent # Конфигурации Debug/Release и заголовок версии находятся рядом со сценарием.
TEMPLATES = ROOT.parent / "newProject/templates" # Использует ту же общую библиотеку firmware-info, что проект 167.
def verify_embedded(image, metadata): # Не позволяет выдать новый манифест за паспорт старого OUT без встроенной версии.
data = image.read_bytes() # Проверяет точные октеты, которые будут отправлены в плату.
if data[:2] != b"\xaa\x08" or not 24 <= len(data) <= 32508: # SCI8 имеет ключ 0x08AA; граница параметров 166 равна (0x3FFF - 129) * 2 = 32508 байт.
raise ValueError("SCI8 is invalid or overlaps the 166 parameter area at byte 32508") # Не допускает повреждения существующих параметров EEPROM.
stamp = datetime.fromisoformat(metadata["built_at_utc"]) # Использует исходное время prebuild без подмены временем конверсии BIN.
build_id = metadata["build_id"] # Ровно восемь ASCII-символов совпадают с четырьмя регистрами firmware-info v1.
words = [1, *metadata["version"], stamp.year, stamp.month * 256 + stamp.day, stamp.hour * 256 + stamp.minute, stamp.second] # Восстанавливает первые восемь регистров независимо от C-упаковщика.
words += [ord(build_id[i]) * 256 + ord(build_id[i + 1]) for i in range(0, 8, 2)] # Восстанавливает четыре слова Build ID в сетевом порядке символов.
expected = struct.pack("<12H", *words) # Загрузочная таблица SCI8 хранит 16-битные слова C28x младшим октетом вперёд.
offset, found = 22, 0 # Первые 22 октета заняты ключом, настройками и аппаратной точкой входа.
while offset + 2 <= len(data): # Каждый шаг потребляет заголовок либо завершает конечный файл; ожидания аппаратуры нет.
count, = struct.unpack_from("<H", data, offset) # Размер очередной секции задан в 16-битных словах C28x.
offset += 2 # Переходит к адресу секции либо концу таблицы при нулевом размере.
if not count: break # Нулевая секция завершает SCI8 и не содержит адреса назначения.
end = offset + 4 + count * 2 # Четыре октета адреса не входят в проверяемую полезную нагрузку секции.
if end > len(data): raise ValueError("Truncated SCI8 section") # Останавливает выпуск при выходе секции за фактический размер файла.
payload = data[offset + 4:end] # Рассматривает загружаемые слова, а не случайное совпадение в заголовке.
found += sum(payload[i:i + 24] == expected for i in range(0, len(payload) - 23, 2)) # Паспорт должен присутствовать один раз на границе слова C28x.
offset = end # Переходит к следующей непересекающейся секции таблицы.
if offset != len(data) or count or found != 1: # Запрещает хвосты, отсутствующий терминатор и манифест от другой компиляции.
raise ValueError(f"SCI8 does not contain exactly one matching prebuild identity: {found}") # Требует полноценную пересборку вместо фиктивного подписания старого BIN.
def find_compiler(): # Сохраняет возможность ручного запуска build_bin.bat без жёсткой привязки к одной версии CCS.
executable = shutil.which("cl2000") or shutil.which("cl2000.exe") # Предпочитает компилятор среды текущего проекта, а не старый hex2000 рядом с BIN.
if executable: return Path(executable).resolve().parents[1] # Возвращает корень toolchain с каталогом bin/hex2000.exe.
installed = sorted(Path("C:/ti").glob("ccs*/ccs/tools/compiler/ti-cgt-c2000_*")) # Для установленного CCS использует только штатные каталоги TI.
installed = [path for path in installed if (path / "bin/hex2000.exe").is_file()] # Не принимает неполную установку за пригодный конвертер.
if not installed: raise FileNotFoundError("Pass --compiler with the C2000 toolchain root") # Явно останавливает ручной запуск без инструментов.
return installed[-1] # Выбирает последнюю найденную установленную цепочку только при отсутствии заданной CCS версии.
def build(compiler, build_dir, artifact): # Выполняет общий postbuild и локальную публикацию после проверки встроенной версии.
sys.path.insert(0, str(TEMPLATES / "c/firmware-info/tools")) # Открывает общие identity.py и c2000_postbuild.py из той же зависимости, что 167.
from c2000_postbuild import postbuild # Не копирует формат манифеста, архивирование и вычисление SHA-256.
metadata_path = build_dir / "generated/firmware_identity.json" # Отсутствующий prebuild должен остановить выпуск вместо использования произвольной версии.
metadata = json.loads(metadata_path.read_text(encoding="utf-8")) # Читает JSON, по которому компилятор получил firmware_identity.h.
if metadata.get("product") != "Balsam-166-PM35" or metadata.get("target") != "TMS320F28335": raise ValueError("Run the 166 prebuild before publishing this image") # Старый generated от ветки 167 не должен выдавать её product за паспорт 166.
output = build_dir / "firmware" # Сохраняет прежнюю для 167 структуру локального каталога выпусков.
with tempfile.TemporaryDirectory(prefix=".identity-release-", dir=build_dir) as directory: # Непроверенные BIN и manifest существуют только во временном каталоге текущего вызова.
stage = Path(directory); staged_image = postbuild(compiler, build_dir, artifact, metadata_path, stage) # Общий код создаёт HEX, BIN, SHA-256 и архив, пока не доступные как готовый выпуск.
verify_embedded(staged_image, metadata) # Не переносит в firmware или Bin результаты от старого OUT либо другой компиляции.
output.mkdir(parents=True, exist_ok=True) # Создаёт каталог публикации только после успешной проверки.
for item in stage.iterdir(): # Сохраняет только результаты этого вызова, не сканируя старые выпуски.
if item.is_dir(): shutil.copytree(item, output / item.name, dirs_exist_ok=True) # Публикует полный архив проверенной сборки.
else: shutil.copy2(item, output / item.name) # Переносит плоские HEX/BIN и контрольные файлы без подмены других исходников.
image = output / staged_image.name # После удаления временного каталога возвращает постоянный путь проверенного BIN.
for file in (image, image.with_suffix(".bin.manifest.json"), image.with_suffix(".bin.sha256"), image.parent / (image.stem.rsplit("_", 1)[0] + ".hex")): # Сохраняет рядом проверенный BIN, его контрольные файлы и прежний Intel HEX для CCS.
shutil.copy2(file, ROOT / "Bin" / file.name) # Основной каталог получает только результат успешной текущей сборки.
shutil.copy2(image.parent / (image.stem.rsplit("_", 1)[0] + ".hex.map"), ROOT / "Bin/UKSSTMS320F28335.map") # Согласовывает прежнюю основную карту конверсии с новым HEX и OUT.
print(ROOT / "Bin" / image.name) # Показывает конкретный BIN, который нужно выбрать в SetGUI вместе с лежащим рядом манифестом.
return image # Позволяет тестам проверить результат без поиска случайного старого файла.
if __name__ == "__main__": # Импорт проверки SCI8 не выполняет сборку или публикацию файлов.
parser = argparse.ArgumentParser(description=__doc__) # Описывает контракт запуска из CCS и build_bin.bat.
parser.add_argument("--compiler", type=Path) # CCS передаёт CG_TOOL_ROOT; ручной вызов может использовать обнаружение установленного компилятора.
parser.add_argument("--build-dir", type=Path, default=PROJECT / "Debug") # Изолирует паспорт и архив выбранной конфигурации.
parser.add_argument("--artifact", default=str(ROOT / "Bin/UKSSTMS320F28335")) # Общий postbuild ожидает полный путь OUT без расширения.
args = parser.parse_args() # Проверяет параметры до выполнения конверсии.
build((args.compiler or find_compiler()).resolve(), args.build_dir.resolve(), args.artifact) # Публикует только образ, соответствующий своему встроенному паспорту.

View File

@@ -0,0 +1,4 @@
{
"product": "Balsam-166-PM35",
"target": "TMS320F28335"
}

View File

@@ -0,0 +1,6 @@
#ifndef BALSAM_FIRMWARE_VERSION_H /* Защищает единственный источник версии продукта 166 от повторного включения. */
#define BALSAM_FIRMWARE_VERSION_H /* Отделяет номер версии приложения от версии логгера и контракта паспорта. */
#define FIRMWARE_VERSION_MAJOR 1U /* Первая версия 166 с читаемым паспортом; это версия продукта, а не заимствованный номер прошивки 167. */
#define FIRMWARE_VERSION_MINOR 0U /* Нулевая минорная версия начинает самостоятельную линию выпусков Balsam-166-PM35. */
#define FIRMWARE_VERSION_PATCH 0U /* Нулевой номер исправления меняется при следующем выпуске продукта. */
#endif /* Генератор читает эти три определения перед компиляцией и помещает их в паспорт BIN. */

View File

@@ -1,33 +1,8 @@
@rem Совместимая точка входа старых make-файлов Debug после подключения паспорта прошивки.
@echo off
rem Сохраняет локальность настроек процесса конвертации.
setlocal
for %%I in ("%~dp0..") do set "SRC_ROOT=%%~fI"
:process_arg
if "%1"=="" goto end_process_arg
set name=%1
set value=
:process_arg_value
if NOT "%value%"=="" set value=%value% %2
if "%value%"=="" set value=%2
shift
if "%2"=="!" goto set_arg
if "%2"=="" goto set_arg
goto process_arg_value
:set_arg
set %name%=%value%
shift
shift
goto process_arg
:end_process_arg
pushd "%SRC_ROOT%\Bin" || exit /b 1
hex2000.exe UKSSTMS320F28335.out -boot -sci8 -map UKSSTMS320F28335.map -o UKSSTMS320F28335.hex -i
set "BUILD_RESULT=%errorlevel%"
popd
if not "%BUILD_RESULT%"=="0" exit /b %BUILD_RESULT%
call "%SRC_ROOT%\build_bin.bat"
rem Сценарий сам находит Debug и проверяет его generated/firmware_identity.json; старые служебные аргументы CCS здесь не нужны.
python "%~dp0build_identity.py"
rem Возвращает ошибку при старом OUT, отсутствующем паспорте либо превышении области параметров.
exit /b %errorlevel%

View File

@@ -0,0 +1,35 @@
"""Подготавливает паспорт 166 тем же генератором firmware-info, что используется в 167.""" # Версия, время UTC и ревизии фиксируются до компиляции.
import argparse # Разбирает одинаковые с проектом 167 пути prebuild.
import importlib.util # Подключает общую реализацию без копирования генератора в прошивку.
import json # Объединяет идентификаторы продукта с версией из C-заголовка.
from pathlib import Path # Нормализует пути независимо от рабочего каталога CCS.
import re # Проверяет единственное допустимое целое определение каждого компонента версии.
import tempfile # Хранит объединённые настройки только на время вызова общего генератора.
def read_version(header): # Сохраняет правила исходного адаптера 167 и ограничения firmware-info v1.
text = re.sub(r"/\*.*?\*/|//[^\n]*", "", header.read_text(encoding="utf-8-sig"), flags=re.S) # Не принимает комментарии или примеры за действующую версию.
result = [] # Накапливает major, minor и patch в порядке регистров контракта.
for part, limit in (("MAJOR", 255), ("MINOR", 255), ("PATCH", 999)): # Два первых компонента ограничены октетом, исправление допускает 0..999.
name = "FIRMWARE_VERSION_" + part # Использует тот же интерфейс заголовка версии, что 167.
definitions = re.findall(r"^\s*#\s*define\s+" + name + r"\b([^\n]*)", text, re.M) # Требует единственного определения без условных альтернатив.
if len(definitions) != 1 or not re.fullmatch(r"(?:0|[1-9][0-9]*)[uU]?", definitions[0].strip()): # Не допускает выражений, шестнадцатеричных литералов и неоднозначной версии.
raise ValueError(f"{header}: define {name} exactly once as a decimal integer") # Останавливает сборку вместо выпуска ложного паспорта.
value = int(definitions[0].strip().rstrip("uU")) # Суффикс C не является частью десятичного числа Python.
if value > limit: # Проверяет границы до записи какого-либо заголовка сборки.
raise ValueError(f"{name} must be in 0..{limit}") # Сохраняет ограничения отображения и Modbus-контракта 167.
result.append(value) # Записывает проверенную компоненту в правильное поле паспорта.
return result # Номер версии формируется только из firmware_version.h.
def prepare(project, templates, output): # Генерирует заголовок и JSON для одной конкретной конфигурации CCS.
settings = json.loads((project / "firmware_identity.json").read_text(encoding="utf-8-sig")) # Сохраняет собственный product 166 и тот же target TMS320F28335.
settings["version"] = read_version(project / "firmware_version.h") # Не дублирует SemVer в двух редактируемых файлах.
spec = importlib.util.spec_from_file_location("shared_firmware_identity", templates / "c/firmware-info/tools/identity.py") # Берёт проверенную общую реализацию из той же зависимости, что 167.
identity = importlib.util.module_from_spec(spec) # Создаёт изолированный модуль генератора без изменения системного Python.
spec.loader.exec_module(identity) # Загружает формат заголовка, UTC и правила формирования Build ID.
with tempfile.TemporaryDirectory(prefix="balsam166-identity-") as directory: # Удаляет только временные объединённые настройки этого вызова.
config = Path(directory) / "settings.json" # Настройки не записываются в исходники и не попадают в историю Git.
config.write_text(json.dumps(settings), encoding="utf-8") # Передаёт общему генератору проверенную версию и product.
return identity.prepare(project, templates, config, output) # Создаёт обязательные generated/firmware_identity.h и JSON до компиляции.
if __name__ == "__main__": # Импорт из теста не запускает изменение файлов сборки.
parser = argparse.ArgumentParser(description=__doc__) # Объясняет назначение prebuild при ручном вызове.
for name in ("project", "templates", "output"): parser.add_argument("--" + name, type=Path, required=True) # Совместим с параметрами prebuild исходного проекта 167.
args = parser.parse_args() # Проверяет обязательные каталоги перед генерацией.
print(json.dumps(prepare(args.project.resolve(), args.templates.resolve(), args.output.resolve()))) # Показывает фактическую идентичность текущей сборки.

View File

@@ -1,16 +1,8 @@
@rem Повторно конвертирует готовый OUT только при совпадении со встроенным паспортом текущего prebuild.
@echo off
rem Ограничивает переменные окружения этим вызовом и не меняет настройки пользователя.
setlocal
set "SRC_ROOT=%~dp0"
set "GIT_COMMIT="
for /f "delims=" %%H in ('git -C "%SRC_ROOT%." rev-parse --short^=8 HEAD') do set "GIT_COMMIT=%%H"
if not defined GIT_COMMIT (
echo ERROR: Cannot determine the Git commit. Make Git available in PATH. 1>&2
exit /b 1
)
pushd "%SRC_ROOT%Bin" || exit /b 1
hex2000.exe UKSSTMS320F28335.out -boot -sci8 -map UKSSTMS320F28335.map -o UKSSTMS320F28335_%GIT_COMMIT%.bin -b
set "BUILD_RESULT=%errorlevel%"
popd
exit /b %BUILD_RESULT%
rem Общий проектный postbuild создаёт BIN, manifest, SHA-256 и проверяет слова паспорта до публикации.
python "%~dp0UKSSTMS320F28335\build_identity.py" %*
rem Передаёт вызывающему процессу фактическую ошибку проверки или конверсии.
exit /b %errorlevel%

View File

@@ -0,0 +1,56 @@
#include <stdint.h> /* Задаёт независимые от host ABI размеры слов паспорта. */
#include <stdio.h> /* Сообщает точную строку нарушенной регрессии. */
#include <string.h> /* Сбрасывает только модель RS-запроса между сценариями. */
#include "crc16.h" /* Использует настоящую CRC прошивки вместо имитации проверки ответа. */
#include "firmware_info.h" /* Подключает действительный адаптер границ 0x1200..0x120B. */
#include "../src/firmware_info_static.c" /* Проверяет тот же общий упаковщик, который компилируется в message.c. */
#define CHECK(value) do { if (!(value)) { printf("FAIL line %d: %s\n", __LINE__, #value); return 1; } } while (0) /* Сохраняет проверки при оптимизации O3. */
#define CMD_MODBUS_3 3U /* Код чтения holding-регистров остаётся прежним. */
#define CMD_MODBUS_6 6U /* Код одиночной записи используется для проверки защиты паспорта. */
#define CNTRL_ADDR 1U /* Тестирует конкретный корректный адрес узла без изменения производственной адресации. */
#define ANSWER_LEN 128 /* Повторяет размер прежней технологической карты. */
#define true 1 /* Повторяет 16-битное представление флага готовности ответа C28x. */
#define LOBYTE(value) ((value) & 255U) /* Сохраняет порядок младшего октета CRC в RS-ответе. */
#define HIBYTE(value) (((value) >> 8) & 255U) /* Сохраняет старший октет контрольной суммы Modbus. */
typedef struct { unsigned int RS_Header[400], buffer[400], flag_TIMEOUT_to_Send; } RS_DATA; /* Даёт настоящим обработчикам только используемые поля UART-окружения. */
static unsigned int sent, irq_mask, irq_calls, reads, writes, command, command_events; /* Наблюдает длину кадра, защиту IRQ и возможное повреждение старого backend. */
static int modbus[ANSWER_LEN + 1]; /* Проверяет чтение технологических регистров прежним маршрутом. */
static struct { unsigned int all; } Modbus[ANSWER_LEN + 1]; /* Поддерживает прежний интерфейс записи в таблицу параметров. */
static unsigned int __disable_interrupts(void) { unsigned int previous = irq_mask; irq_mask = 1U; ++irq_calls; return previous; } /* Выявляет лишнюю блокировку IRQ при чтении неизменяемого паспорта. */
static void __restore_interrupts(unsigned int previous) { irq_mask = previous; } /* Проверяет сохранение исходной маски для снимков логгера. */
static void RS_Send(RS_DATA *rs, unsigned int *data, unsigned long length) { (void)rs; (void)data; sent = (unsigned int)length; } /* Перехватывает готовый кадр без реального UART. */
unsigned int ump_log_read(unsigned int address) { ++reads; return address == 0x1000U ? 0x554DU : address; } /* Делает обращения к старому логгеру видимыми и узнаваемыми. */
int ump_log_write(unsigned int address, unsigned int value) { (void)address; (void)value; ++writes; return 1; } /* Любая ошибочная запись паспорта в backend станет видна тесту. */
static void UMP_commands_write(unsigned int value) { command = value; } /* Сохраняет наблюдение штатной команды регистра 127. */
static void ump_log_rs(void) { ++command_events; } /* Проверяет сохранение прежней регистрации команды RS. */
#include "ump_can_protocol.h" /* Тестирует реальный адаптер CAN 166, сохранивший двенадцатисловный логгер v4. */
/* PRODUCTION_MODBUS */
static void request(RS_DATA *rs, unsigned int address, unsigned int count) /* Формирует уже проверенный транспортом заголовок для настоящих обработчиков. */
{ /* Здесь не подменяется алгоритм чтения или упаковки ответа. */
memset(rs, 0, sizeof(*rs)); sent = 0; /* Не переносит данные предыдущего ответа в новый запрос. */
rs->RS_Header[2] = address >> 8; rs->RS_Header[3] = address & 255U; /* Упаковывает исходный адрес старшим октетом вперёд. */
rs->RS_Header[4] = count >> 8; rs->RS_Header[5] = count & 255U; /* Упаковывает число слов либо значение одиночной записи. */
} /* Приём полного CRC-запроса остаётся вне этой локальной проверки обработчиков. */
int main(void) /* Проверяет перенос паспорта и сохранность старых границ протокола. */
{ /* Эталон задан независимо от макроса общего C-инициализатора. */
const unsigned int expected[12] = {1, 1, 0, 0, 2026, 0x0A10, 0x0834, 22, 0x3132, 0x3334, 0x6162, 0x6364}; /* Формат v1, версия 1.0.0, UTC 2026-10-16 08:52:22, Build ID 1234abcd. */
RS_DATA rs; unsigned int words[UMP_CAN_MAX_WORDS], error, i, before; /* Использует те же ёмкости ответа, что действующий служебный CAN. */
request(&rs, 0x1200U, 12U); ReceiveCommandModbus3(&rs); CHECK(sent == 29U && rs.buffer[1] == 3U && rs.buffer[2] == 24U); /* Полный паспорт отвечает стандартным FC03 без хвостовых нулей. */
for (i = 0; i < 12U; ++i) CHECK(((rs.buffer[3 + i * 2] << 8) | rs.buffer[4 + i * 2]) == expected[i]); /* Проверяет все поля, порядок символов и октетов независимо от производственной упаковки. */
CHECK(get_crc_16(0xFFFFU, rs.buffer, sent) == 0U && reads == 0U && irq_calls == 0U); /* Паспорт защищён настоящей CRC и не обращается к логгеру или маске IRQ. */
request(&rs, 0x120BU, 1U); ReceiveCommandModbus3(&rs); CHECK(sent == 7U && rs.buffer[3] == 0x63U && rs.buffer[4] == 0x64U); /* Последнее слово поддерживает частичное чтение в пределах контракта 167. */
request(&rs, 0x120BU, 2U); ReceiveCommandModbus3(&rs); CHECK(sent == 5U && rs.buffer[1] == 0x83U && rs.buffer[2] == 2U); /* Пересечение верхней границы не раскрывает соседнюю память. */
request(&rs, 0x11FFU, 2U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Запрос из несуществующего регистра перед паспортом не считается допустимым. */
request(&rs, 0x1200U, 0U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Пустое чтение не принимает произвольный контекст за паспорт. */
request(&rs, 0xFFFFU, 12U); ReceiveCommandModbus3(&rs); CHECK(rs.buffer[2] == 2U); /* Проверка диапазона выдерживает переполнение 16-битного адреса. */
request(&rs, 0x1200U, 99U); ReceiveCommandModbus6(&rs); CHECK(sent == 5U && rs.buffer[1] == 0x86U && rs.buffer[2] == 2U && writes == 0U); /* Запись паспорта явно отвергается, не попадая в сохранённые параметры или логгер. */
CHECK(ump_can_prepare(3U, 0x1200U, 12U, words, &error) == 12U && error == 0U); /* Служебный CAN читает тот же паспорт без переноса остального драйвера 167. */
for (i = 0; i < 12U; ++i) CHECK(words[i] == expected[i]); /* Исключает расхождение паспорта между RS и CAN. */
ump_can_prepare(3U, 0x120BU, 2U, words, &error); CHECK(error == 2U); ump_can_prepare(6U, 0x1200U, 0U, words, &error); CHECK(error == 2U && writes == 0U); /* CAN также запрещает выход за паспорт и запись в него. */
modbus[5] = 0x1234; request(&rs, 5U, 1U); ReceiveCommandModbus3(&rs); CHECK(sent == 10U && rs.buffer[3] == 0x12U && rs.buffer[4] == 0x34U); /* Исторический ответ технологического регистра и его хвост сохранены. */
request(&rs, 0x1000U, 16U); ReceiveCommandModbus3(&rs); CHECK(sent == 37U && rs.buffer[3] == 0x55U && rs.buffer[4] == 0x4DU && reads == 16U && irq_calls == 1U && irq_mask == 0U); /* Снимок статуса логгера сохраняет прежний маршрут и защиту IRQ. */
before = reads; CHECK(ump_can_prepare(3U, UMP_LOG_DATA, 12U, words, &error) == 12U && error == 0U && reads == before + 12U && UMP_LOG_WORDS == 12U); /* Текущая запись v4 остаётся двенадцатисловной и не заменяется реализацией 167. */
request(&rs, 127U, 0x55U); ReceiveCommandModbus6(&rs); CHECK(command == 0x55U && command_events == 1U && sent == 10U); /* Штатная команда управления проходит прежний обработчик без изменения её смысла. */
puts("PASS: firmware-info RS/CAN, all 12 words, CRC, bounds, read-only, legacy registers and logger v4"); /* Подтверждает проверенный перенос без заявления об испытании аппаратной платы. */
return 0; /* Завершает все поведенческие сценарии реального кода обработчиков. */
} /* Остальные аппаратные алгоритмы не участвуют в тесте и не изменяются доработкой. */

View File

@@ -0,0 +1,57 @@
"""Проверяет точечный перенос паспорта 167 в действующие обработчики 166.""" # Использует реальные функции, общий статический порт и независимый эталон регистра.
import importlib.util # Загружает проверку встроенного паспорта без запуска конверсии или сборки.
import json # Формирует независимый тестовый manifest.
import os # Подключает runtime установленного GCC только дочерним процессам.
from pathlib import Path # Находит production-исходники и общую библиотеку относительно теста.
import re # Извлекает функции Modbus без копирования их реализации в тест.
import struct # Создаёт минимальный SCI8 с известными загрузочными словами.
import subprocess # Компилирует настоящие обработчики под оптимизацией O3.
import sys # Подключает общий анализатор паспорта, который использует SetGUI.
import tempfile # Изолирует временные заголовки и исполняемые файлы от выпуска прошивки.
ROOT = Path(__file__).resolve().parents[2] # Корень 166 содержит проверяемый message.c.
TEMPLATES = ROOT.parent / "newProject/templates" # Использует ту же общую зависимость, что проект 167 и текущая сборка.
def load(name, path): # Импортирует проектный скрипт без его CLI и побочных эффектов postbuild.
spec = importlib.util.spec_from_file_location(name, path); module = importlib.util.module_from_spec(spec) # Создаёт независимый модуль с точным путём источника.
spec.loader.exec_module(module); return module # Возвращает реальные функции проверки версии и SCI8.
builder = load("balsam166_build", ROOT / "UKSSTMS320F28335/build_identity.py") # Проверяет именно выпускной валидатор проекта.
prepare = load("balsam166_prepare", ROOT / "UKSSTMS320F28335/prepare_identity.py") # Проверяет тот же парсер SemVer, что выполняет CCS prebuild.
source = re.sub(r"/\*.*?\*/|//[^\n]*", "", (ROOT / "Source/Internal/message.c").read_text(encoding="utf-8"), flags=re.S) # Исключает скобки из комментариев при выделении определений.
def function(name): # Извлекает рабочую функцию с произвольной вложенностью условий.
match = re.search(r"(?:static\s+)?void\s+" + name + r"\([^;{}]*\)\s*\{", source); assert match, name # Не принимает вызов или прототип за определение.
end, depth = match.end(), 1 # Начинает подсчёт после открывающей скобки функции.
while depth: depth += (source[end] == "{") - (source[end] == "}"); end += 1 # Находит внешнюю закрывающую скобку настоящего алгоритма.
return source[match.start():end] # Сохраняет все операторы производственного обработчика.
model = Path(__file__).with_name("port_test.c").read_text(encoding="utf-8") # Окружение содержит только UART/IRQ и наблюдаемый backend логгера.
model = model.replace("/* PRODUCTION_MODBUS */", "\n".join(function(name) for name in ("modbus_exception", "ReceiveCommandModbus3", "ReceiveCommandModbus6"))) # Подставляет реальный код, который попадёт в прошивку.
compiler = os.environ.get("CC", "C:/msys64/mingw64/bin/gcc.exe") # Сохраняет возможность запускать тест доступным C99-компилятором.
environment = dict(os.environ); environment["PATH"] = str(Path(compiler).parent) + os.pathsep + environment.get("PATH", "") # Не меняет окружение CCS или приложения.
with tempfile.TemporaryDirectory(prefix="firmware166-test-") as directory: # Удаляет только собственные временные данные теста.
temporary = Path(directory); cfile = temporary / "port.c"; exe = temporary / "port.exe" # Разделяет C-модель и результат исполнения.
values = dict(zip(("FIRMWARE_VERSION_MAJOR", "FIRMWARE_VERSION_MINOR", "FIRMWARE_VERSION_PATCH", "FI_YEAR", "FI_MONTH", "FI_DAY", "FI_HOUR", "FI_MINUTE", "FI_SECOND"), (1, 0, 0, 2026, 10, 16, 8, 52, 22))) # Детерминированный паспорт независим от времени запуска теста.
values.update({f"FI_ID_{i}": ord(char) for i, char in enumerate("1234abcd")}) # Даёт известную последовательность ASCII-слов для проверки порядка октетов.
(temporary / "firmware_identity.h").write_text("\n".join(f"#define {name} {value}U /* Проверочная константа паспорта v1. */" for name, value in values.items()), encoding="utf-8") # Сохраняет fixture заголовка в требуемой кодировке.
cfile.write_text(model, encoding="utf-8") # Сохраняет извлечённые русские исходники без изменения production-файла.
command = [compiler, "-std=c99", "-O3", "-Wall", "-Wextra", "-Werror", "-I" + str(temporary), "-I" + str(ROOT / "Source/Internal/Include"), "-I" + str(TEMPLATES / "c/firmware-info/include"), str(cfile), str(ROOT / "Source/Internal/crc16.c"), "-o", str(exe)] # Проверяет типы и реальные CRC вместе с обработчиками RS/CAN.
subprocess.run(command, env=environment, check=True); subprocess.run([str(exe)], env=environment, check=True, timeout=10) # Ошибка типов, поведения или зависание делают тест неуспешным.
metadata = {"schema": 1, "product": "Balsam-166-PM35", "target": "TMS320F28335", "version": [1, 0, 0], "build_id": "1234abcd", "built_at_utc": "2026-10-16T08:52:22+00:00", "identity_contract": 1, "identity_register_count": 12} # Фиксирует весь контракт manifest, который должен принять существующий GUI.
words = [1, 1, 0, 0, 2026, 0x0A10, 0x0834, 22, 0x3132, 0x3334, 0x6162, 0x6364] # Независимый эталон совпадает с показанным протоколом, а не берётся из C-инициализатора.
data = b"\xaa\x08" + bytes(20) + struct.pack("<HHH12HH", 12, 0, 0x8000, *words, 0) # Создаёт корректный SCI8 с одним 12-словным блоком и терминатором.
image = temporary / "firmware.bin"; image.write_bytes(data); builder.verify_embedded(image, metadata) # Выпуск должен принять образ с достоверно совпавшим паспортом.
for broken in (data[:-2], data + b"\0\0", data[:28] + bytes(24) + data[52:], bytes(32509)): # Проверяет усечённый файл, хвост, чужие слова паспорта и пересечение параметров EEPROM.
image.write_bytes(broken) # Подменяет только временный проверяемый образ.
try: builder.verify_embedded(image, metadata) # Настоящий postbuild-валидатор должен отклонить каждый дефект.
except (ValueError, struct.error): pass # Ожидаемый отказ подтверждает защиту выпуска.
else: raise AssertionError("Invalid or stale SCI8 was accepted") # Не допускает фиктивного manifest поверх старой прошивки.
image.write_bytes(data); settings = temporary / "metadata.json"; settings.write_text(json.dumps(metadata), encoding="utf-8") # Восстанавливает корректный временный выпуск для проверки GUI.
sys.path.insert(0, str(TEMPLATES / "c/firmware-info/tools")); from identity import seal # Использует общий формат манифеста, применяемый в 167.
seal(image, settings) # Подписывает точные байты тестового BIN общим алгоритмом SHA-256.
native = ROOT.parent / "newProject/SETGUI/src/gui_desktop/native/setprotocol.dll" # Существующий GUI содержит ту же native-библиотеку CRC, которую импортирует анализатор паспорта.
if native.is_file(): os.environ.setdefault("SETPROTOCOL_LIBRARY", str(native)) # Сохраняет явно заданный путь пользователя и подключает установленный GUI без изменения файлов зависимости.
gui_python = ROOT.parent / "newProject/SETGUI/third_party/templates/python" # Проверяет фактическую vendored-копию анализатора текущего SetGUI, которая может отличаться от основной ветки templates.
sys.path.insert(0, str(gui_python if (gui_python / "set_devices/firmware_identity.py").is_file() else TEMPLATES / "python")); from set_devices.firmware_identity import inspect_image, decode_identity # Без установленного GUI использует тот же общий контракт библиотеки.
identity = inspect_image(image, expected_product="Balsam-166-PM35"); assert identity == decode_identity(words) # Версия из BIN и версия из ответа платы должны сравниваться без адаптации GUI.
try: inspect_image(image, expected_product="Balsam-167-PM35") # Перенос проверки не должен разрешать выдавать прошивку 166 за 167.
except ValueError: pass # Существующая защита GUI отличает product двух плат.
else: raise AssertionError("166 firmware accepted as 167") # Ошибка product делает перенос небезопасным для выбора модели.
assert prepare.read_version(ROOT / "UKSSTMS320F28335/firmware_version.h") == [1, 0, 0] # Проверяет единственный источник начальной версии текущего продукта.
print("PASS: embedded identity, stale/truncated/oversize rejection, exact manifest and unchanged SetGUI decoder") # Подтверждает проверку выпуска и совместимость без физического обмена с платой.

View File

@@ -1,5 +1,6 @@
#include "../../Source/Internal/log_to_mem.c"
#include "../../Source/Internal/Include/ump_can_protocol.h"
uint16_t firmware_info_static_read(unsigned index) { (void)index; return 0U; } /* Изолирует проверку старого логгера от данных сборки; реальные слова паспорта проверяются отдельным port_test.c. */
struct mock_gpio GpioDataRegs;
int READY = 0, Desk = 6, Mode = 5;
int modbus[128];

View File

@@ -11,21 +11,24 @@ import subprocess
import tempfile
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--compiler', default=shutil.which('clang') or shutil.which('cl'))
parser.add_argument('--compiler', default=shutil.which('clang') or shutil.which('cl') or shutil.which('gcc') or ('C:/msys64/mingw64/bin/gcc.exe' if Path('C:/msys64/mingw64/bin/gcc.exe').is_file() else None)) # Поддерживает имеющийся GCC для проверки нового include-пути без обязательной установки другого host-компилятора.
args = parser.parse_args()
if not args.compiler:
parser.error('Provide --compiler pointing to clang.exe or cl.exe')
parser.error('Provide --compiler pointing to clang.exe, cl.exe or gcc.exe') # Явно перечисляет поддерживаемые инструменты проверки, не меняя компилятор самой прошивки.
root = Path(__file__).resolve().parents[2]
with tempfile.TemporaryDirectory(prefix='ump-recorder-') as directory:
output = Path(directory)
library = output / 'recorder_test.dll'
includes = [root / 'tests/ump_logger/stubs', root / 'Source/Internal/Include']
includes = [root / 'tests/ump_logger/stubs', root / 'Source/Internal/Include', root.parent / 'newProject/templates/c/firmware-info/include'] # Адаптер CAN теперь использует общий статический интерфейс паспорта, как production-сборка 166.
source = root / 'tests/ump_logger/recorder_test.c'
if Path(args.compiler).stem.lower() == 'cl':
command = [args.compiler, '/nologo', '/LD', '/GS-', '/wd4068', '/wd4312',
*['/I' + str(p) for p in includes], str(source),
'/Fo' + str(output / 'recorder.obj'), '/link', '/NOENTRY',
'/NODEFAULTLIB', '/OUT:' + str(library)]
elif Path(args.compiler).stem.lower() in ('gcc', 'cc'): # MinGW использует обычную DLL с CRT и не принимает специальные параметры компоновщика Clang/lld.
command = [args.compiler, '-shared', '-O3', '-Wno-unknown-pragmas', '-Wno-int-to-pointer-cast', *['-I' + str(p) for p in includes], str(source), '-o', str(library)] # Компилирует тот же существующий recorder_test.c и production-логгер без копирования его алгоритма.
os.environ['PATH'] = str(Path(args.compiler).resolve().parent) + os.pathsep + os.environ.get('PATH', '') # Даёт дочернему Python доступ к runtime DLL выбранного MinGW только в этом тестовом процессе.
else:
command = [args.compiler, '-shared', '-nostdlib', '-fuse-ld=lld',
'-Wl,/noentry,/nodefaultlib', '-Wno-unknown-pragmas',