Author SHA1 Message Date
Andrey a197437985 Исправить ядро SCI UlitCo и отображение хода прошивки
Загружать RAM-ядро через переход по адресу 0 в _InitBoot,
повторно использовать поток Qt и передавать сигналы в GUI.
Показывать точный этап ошибки, изменения процентов и время сеанса.

Проверено: реальная сборка ядра TI и аппаратная передача ИБП
на скоростях 57600 и 115200 бод.
2026-11-13 19:23:40 +03:00
Andrey ae018a1347 Исправить окончания строк командных файлов Windows
Выдавать BAT и CMD с CRLF при получении из Git. Общий LF приводил к пропуску команд postbuild и экспорта SCI-комплекта после переноса на другой ПК.
2026-11-13 17:06:56 +03:00
Andrey 80d8fe1794 Исправить вход UlitCo в SCI через Monitor
Передавать контрольную сумму команды перехода в загрузчик.
Отправлять Monitor на 468750 бод и затем включать скорость SCI из JSON.
Обновить эталонные проверки входа в загрузчик.
2026-11-13 17:06:39 +03:00
Andrey d057df7698 Добавить прошивку F28335 через SCI и подготовку Python
Подключить сборку Flash BIN и RAM-ядра TI через templates.
Передавать настройки CAN и RS из JSON и проверять окружение CCS/Keil.
Добавить общий C99-протокол и проверки обмена SCI.
2026-11-13 17:02:33 +03:00
Andrey f9d369d032 feat(f28335-rs-boot): добавьте постоянный загрузчик по RS
Принимайте INITLOAD, LOAD и TFLASH общим автоматом с повторной
проверкой SCI8/FAB1 до стирания и сохранением сектора A.

Сохраняйте параметры прошивки в паспорте prebuild, выпускайте
независимые образы загрузчика и приложения с manifest и SHA-256.

Проверено: шесть испытаний модели, включая обрывы записи и восстановление.
2026-11-13 15:07:55 +03:00
33 changed files with 1442 additions and 10 deletions
+5
View File
@@ -4,3 +4,8 @@
*.pdf binary
*.png binary
# CMD.exe требует CRLF в BAT, иначе после переноса Git часть команд разбирается неверно.
*.bat text eol=crlf
# Командные файлы CMD выполняются тем же интерпретатором Windows и требуют CRLF.
*.cmd text eol=crlf
+3
View File
@@ -27,3 +27,6 @@ Desktop.ini
# Generated native library (built before packaging)
python/protocan/native/
# Кэш pytest создаётся проверками автоматически и не входит в исходники общей библиотеки.
.pytest_cache/
+45
View File
@@ -0,0 +1,45 @@
# Загрузчик F28335 по RS для periph_28335
Постоянный загрузчик принимает `INITLOAD` (58), `LOAD` (51) и `TFLASH` (55),
совместимые с Flash RS в терминале `periph_28335` SETGUI. Ядро не зависит от
UART, GPIO, ОС и Qt. Оно использует существующий валидатор и writer
`c/protocan-boot/ports/tms320f28335`, сохраняя единый формат SCI8/FAB1.
Слои: клиент periph_28335 → автомат RS → проверка SCI8/FAB1 → callbacks платы
→ TI Flash API. Передаются октеты; адреса CPU считаются 16-битными словами.
| Файл | Назначение и зависимости |
|---|---|
| `include/f28335_rs_boot.h`, `src/f28335_rs_boot.c` | Переносимый автомат, зависит от protocan-boot |
| `ports/tms320f28335/flash_api.c` | Официальный TI API v2.10, заголовки устройства, частота F28335_RS_CPU_HZ |
| `ports/tms320f28335/boot.cmd` | Постоянный сектор A, отдельные RAMAPI, RAMFUNCS и стек |
| `tools/build.py` | Сборка boot и выпуск проверенных Flash OUT/SCI8 с manifest/SHA-256 |
| `tests/test_boot.py` | Настоящий автомат и writer в модели RAM/Flash без оборудования |
Приложение занимает `[0x300000,0x338000)`, первые 16 слов содержат FAB1.
Сектор A и переход `0x33FFF6` принадлежат постоянному загрузчику.
`INITLOAD` разрешает только staging `0x200000`, чётный размер 24..130560
октетов. Ошибочные CRC, таймаут и ошибка UART отзывают подтверждение LOAD.
Перед erase повторно проверяются RAM, CRC32, все адреса, entry и FAB1.
Общий writer пишет FAB1 последним и проверяет физическую Flash.
Ошибка не удаляет recovery; перезапуск разрешён после успешного ACK.
Контракт callbacks: `stage_write(user, offset, octet)` и `stage.read(user,
offset, buffer, size)` обслуживают RAM относительно начала файла;
`transmit(user, data, size)` возвращает true после физического окончания TX;
`program(user, image)` вызывает общий Flash writer; `reboot(user)` сбрасывает
контроллер. Все чтения/записи должны завершиться полностью либо вернуть false.
Прерывания в постоянном загрузчике запрещены, watchdog выключен на время
стирания. Межбайтовый таймаут аппаратный порт передаёт в `f28335_rs_abort`.
Быстрый старт: выделить `f28335_rs_boot_t` в RAM, заполнить пять callbacks,
вызвать `f28335_rs_init(&boot, &port, 16)` и передавать каждый октет SCI
через `f28335_rs_feed(&boot, octet)`. До запуска приложения вызвать
`f28335_flash_app_validate`, после успеха перейти на возвращённый entry.
Аппаратные таймауты, выключение силовых выходов и выбор скорости задаёт плата.
Подключено в `prolitarski_ups`: SCI-A/RS-422, 115200 8N1, адрес 16;
Monitor №93=1 в остановленном состоянии запрашивает boot через watchdog.
Первичная установка требует записи двух OUT через JTAG с сохранением
сектора A при загрузке приложения. Все последующие RS-обновления меняют B–H.
Повреждённое приложение оставляет устройство в RS recovery после сброса.
+23
View File
@@ -0,0 +1,23 @@
#ifndef F28335_RS_BOOT_H /* Один контракт обслуживает постоянный загрузчик и его модель на ПК. */
#define F28335_RS_BOOT_H /* Исключает повторные объявления при подключении аппаратного порта. */
#include "../../protocan-boot/ports/tms320f28335/f28335_flash_update.h" /* Повторно использует проверку SCI8/FAB1 и запись с защитой сектора A. */
#define F28335_RS_STAGE 0x200000UL /* Адрес слова внешней RAM совпадает с INITLOAD в periph_28335. */
#define F28335_RS_LIMIT F28335_AB_SLOT_BYTES /* Проверяемый SCI8 ограничен 130560 октетами общего валидатора F28335. */
typedef struct { /* Аппаратная плата владеет памятью и UART; ядро владеет только состоянием протокола. */
bool (*stage_write)(void *, uint32_t, unsigned int); /* Записывает один октет по относительному смещению staging RAM. */
f28335_boot_io_t stage; /* Читает октеты staging для повторной CRC32 и проверки всех адресов перед erase. */
bool (*transmit)(void *, const unsigned int *, unsigned int); /* Передаёт полный ответ и дожидается последнего стопового бита. */
unsigned int (*program)(void *, f28335_flash_update_t *); /* Возвращает 0 после записи и проверки, код TI либо 1004/1006 при защите или ошибке проверки; управление остаётся в секторе A. */
void (*reboot)(void *); /* Выполняет reset только после успешного ответа о завершении записи. */
void *user; /* Независимый контекст UART, Flash и модели испытаний. */
} f28335_rs_port_t; /* Аппаратный порт не собирает кадры и не дублирует алгоритм проверки образа. */
typedef struct { /* Экземпляр допускает работу без heap и без прерываний. */
f28335_rs_port_t port; /* Сохраняет callbacks на весь сеанс обновления. */
f28335_flash_update_t image; /* Ограниченный индекс адресных секций и страничный кеш общего валидатора. */
uint32_t length, position, expected, received, crc32; /* Длины и позиции считаются октетами, независимо от sizeof(char) C28x. */
unsigned int address, command, crc16, wire_crc, loaded, fault, header[16]; /* Заголовок вмещает максимальный фиксированный запрос TFLASH на 16 октетов. */
} f28335_rs_boot_t; /* Загруженный блок никогда не исполняется как произвольный код RAM. */
void f28335_rs_init(f28335_rs_boot_t *s, const f28335_rs_port_t *port, unsigned int address); /* Настраивает адрес 1..255 и сбрасывает разрешение программирования. */
void f28335_rs_abort(f28335_rs_boot_t *s); /* Сбрасывает неполный кадр и подтверждение LOAD при таймауте или аппаратной ошибке SCI. */
void f28335_rs_feed(f28335_rs_boot_t *s, unsigned int value); /* Принимает один восьмибитный октет совместимых INITLOAD/LOAD/TFLASH. */
#endif /* Завершает публичный интерфейс общего RS-загрузчика. */
@@ -0,0 +1,28 @@
MEMORY /* Все адреса и размеры относятся к 16-битным словам CPU C28x. */
{ /* Постоянный загрузчик использует только сектор A и внутреннюю RAM. */
PAGE 0: /* Исполняемый код и адреса загрузки из Flash. */
FLASHA : origin = 0x338000, length = 0x007F80 /* Область постоянного boot заканчивается до служебной области CSM. */
BEGIN : origin = 0x33FFF6, length = 0x000002 /* Два слова аппаратного перехода Boot ROM в режиме Flash. */
RAMAPI : origin = 0x008000, length = 0x001800 /* 6144 слова предназначены для копии официального TI Flash API. */
RAMFUNCS : origin = 0x00F800, length = 0x000800 /* InitFlash выполняется из RAM, не затрагивая код самого API. */
RESET : origin = 0x3FFFC0, length = 0x000002 /* Вектор reset находится в ROM и не программируется. */
PAGE 1: /* Данные загрузчика не зависят от размещения будущего приложения. */
RAMM0 : origin = 0x000058, length = 0x0003A8 /* Слова 50..57 сохранены для запроса boot через watchdog reset. */
RAMM1 : origin = 0x000400, length = 0x000400 /* Стек на 1024 слова вмещает буфер writer и контекст runtime. */
RAMDATA : origin = 0x00E000, length = 0x001800 /* Индекс SCI8, состояния протокола и аппаратные данные находятся в RAM. */
} /* Внешняя RAM используется через callbacks и не содержит исполняемого кода. */
SECTIONS /* C runtime загрузчика, в отличие от приложения, целиком сохраняется в A. */
{ /* Размещение API указано до общей .text, чтобы извлечь все его код и константы. */
codestart : > BEGIN, PAGE = 0 /* Штатный TI startup запрещает watchdog и вызывает _c_int00 загрузчика. */
flash_api : { -lFlash28335_API_V210.lib(.text) -lFlash28335_API_V210.lib(.econst) } LOAD = FLASHA, RUN = RAMAPI, LOAD_START(_FlashApiLoadStart), LOAD_END(_FlashApiLoadEnd), RUN_START(_FlashApiRunStart), PAGE = 0 /* Ни одна функция TI API не исполняется из занятого банка Flash. */
.text : > FLASHA, PAGE = 0 /* Протокол, валидатор и собственный runtime загрузчика остаются после обновления приложения. */
.cinit : > FLASHA, PAGE = 0 /* Инициализация данных не зависит от целостности B–H. */
.pinit : > FLASHA, PAGE = 0 /* Поддерживает стандартную секцию инициализации COFF. */
.econst : > FLASHA, PAGE = 0 /* Таблицы и константы загрузчика также защищены от erase B–H. */
.switch : > FLASHA, PAGE = 0 /* Таблицы переходов автомата не находятся в заменяемом приложении. */
ramfuncs : LOAD = FLASHA, RUN = RAMFUNCS, LOAD_START(_RamfuncsLoadStart), LOAD_END(_RamfuncsLoadEnd), RUN_START(_RamfuncsRunStart), PAGE = 0 /* InitFlash переносится до изменения Flash wait states. */
.stack : > RAMM1, PAGE = 1 /* Стек отделён от handoff и буфера внешней RAM. */
.ebss : > RAMDATA, PAGE = 1 /* Нулевая инициализация касается только состояния boot. */
.esysmem : > RAMDATA, PAGE = 1 /* Оставляет корректную секцию runtime без использования heap в алгоритме. */
.reset : > RESET, PAGE = 0, TYPE = DSECT /* ROM-вектор не попадает в программируемый OUT. */
} /* Пароли CSM, OTP и сектора приложения не создаются в образе загрузчика. */
@@ -0,0 +1,33 @@
#include "DSP2833x_Device.h" /* Использует официальные описания регистров выбранной платы F28335. */
#include "../../include/f28335_rs_boot.h" /* Получает общий writer с повторной проверкой staging перед erase. */
#include "Flash2833x_API_Library.h" /* Официальный TI API v2.10 выполняет физические операции внутренней Flash. */
#include <string.h> /* Копирует код и таблицы TI API в RAM до изменения общего банка Flash. */
extern Uint16 FlashApiLoadStart, FlashApiLoadEnd, FlashApiRunStart; /* LOAD/RUN-символы создаёт linker постоянного загрузчика. */
static FLASH_ST api_status; /* Диагностика TI хранится в RAM и доступна во время занятости Flash. */
static unsigned int api_error; /* Код последней операции TI передаётся клиенту без подмены успехом. */
static bool erase(void *user) /* Стирает исключительно семь секторов приложения B–H. */
{ /* Сам API исполняется из RAM; вызывающий код продолжит работу из сохранённого сектора A. */
(void)user; api_error = Flash_Erase(0x00FEU, &api_status); return api_error == STATUS_SUCCESS; /* В TI v2.10 бит 0 выбирает A, маска FE исключает его и сохраняет recovery; результат сохраняет диагностику API. */
} /* Ненулевой результат запрещает программирование данных. */
static bool program(void *user, uint32_t address, uint16_t *data, unsigned int words) /* Записывает и немедленно проверяет одну ограниченную порцию. */
{ /* Дополнительная аппаратная проверка диапазона защищает boot независимо от wire-парсера. */
Uint16 result; (void)user; /* Результат официального API не подменяется успешным кодом протокола. */
if (!words || address < F28335_FLASH_APP_START || address >= F28335_FLASH_APP_STOP || words > F28335_FLASH_APP_STOP - address) return false; /* Исключает сектор A, CSM, OTP и переполнение верхней границы. */
result = Flash_Program((Uint16 *)address, data, words, &api_status); /* Данные и рабочий буфер находятся в RAM. */
if (!result) result = Flash_Verify((Uint16 *)address, data, words, &api_status); /* Подтверждает физическую запись до перехода к следующей порции. */
api_error = result; return result == STATUS_SUCCESS; /* Ошибка оставляет FAB1 неопубликованным, сохраняет код TI и RS recovery. */
} /* Все прерывания остаются запрещёнными на протяжении жизни постоянного загрузчика. */
uint16_t F28335Rs_ReadFlash(void *user, uint32_t address) /* Читает физическое слово для общей проверки приложения перед запуском. */
{ /* Общий валидатор ограничивает адреса диапазоном B–H. */
(void)user; return *(volatile Uint16 *)address; /* Чтение применяется только вне активной операции Flash API. */
} /* Стёртые промежутки проверяются вместе с загруженными секциями. */
unsigned int F28335Rs_Program(void *user, f28335_flash_update_t *image) /* Callback работает из сектора A и возвращает полный статус, совместимый с Flash RS periph_28335. */
{ /* Проект задаёт реальный SYSCLK в герцах через F28335_RS_CPU_HZ при компиляции. */
f28335_flash_ops_t ops; (void)user; /* Таблица операций не содержит обращений к приложению из B–H. */
if (CsmRegs.CSMSCR.bit.SECURE) return 0x1004U; /* Не начинает стирание защищённого CSM устройства и передаёт клиенту стандартный статус защиты. */
memcpy(&FlashApiRunStart, &FlashApiLoadStart, &FlashApiLoadEnd - &FlashApiLoadStart); /* Все .text и .econst TI API копируются во внутреннюю RAM. */
Flash_CPUScaleFactor = (Uint32)(1048576.0L * 200.0L * F28335_RS_CPU_HZ / 1000000000.0L); /* Формула IQ20 из TI v2.10 учитывает фактическую частоту CPU, единицы — Гц. */
Flash_CallbackPtr = 0; ops.erase = erase; ops.program = program; ops.read = F28335Rs_ReadFlash; ops.user = 0; /* Callback во Flash во время erase/program запрещён. */
api_error = 0U; if (f28335_flash_update_program(image, &ops)) return 0U; /* Повторная CRC staging, запись FAB1 последним и CRC физической Flash находятся в общем writer; ноль означает подтверждённый успех. */
return api_error ? api_error : 0x1006U; /* Ошибка TI сохраняет свой код, отказ структуры/readback без ошибки API получает отдельный ненулевой статус. */
} /* После отказа управление остаётся в постоянном загрузчике и допускает новый LOAD. */
+79
View File
@@ -0,0 +1,79 @@
#include "../include/f28335_rs_boot.h" /* Единственная реализация автомата используется платой и host-тестами. */
#include <string.h> /* Сбрасывает состояние без динамического выделения памяти. */
static unsigned int crc16(unsigned int crc, unsigned int value) /* CRC16 Modbus защищает запросы и ответы существующего periph_28335. */
{ /* Все операции явно ограничены 16 битами и подходят для 16-битного int C28x. */
unsigned int bit; /* Каждый принятый октет содержит ровно восемь значащих битов. */
crc ^= value & 255U; /* Старшие биты аппаратного регистра SCI не являются данными протокола. */
for (bit = 0U; bit < 8U; ++bit) crc = (crc & 1U) ? ((crc >> 1) ^ 0xA001U) : (crc >> 1); /* Отражённый полином A001 соответствует CRC16 Modbus. */
return crc & 65535U; /* На ПК unsigned int шире C28x, поэтому маска сохраняет одинаковый результат. */
} /* Завершает переносимый шаг CRC. */
static uint32_t get32(const unsigned int *p) /* Поля BIOS передаются младшим октетом вперёд, в отличие от адреса внутри SCI8. */
{ /* Приведение перед сдвигом исключает потерю старших битов на 16-битном int. */
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); /* Возвращает адрес слова либо длину октетов по назначению поля. */
} /* Формат запроса совпадает с periph28335_firmware.request. */
static bool reply(f28335_rs_boot_t *s, unsigned int command, unsigned int status) /* TFLASH сообщает конкретный статус, остальные успешные команды — короткий ACK. */
{ /* Ответ не содержит хвостового паддинга, который клиент periph_28335 считает необязательным. */
unsigned int frame[6], crc = 0xFFFFU, i, size = command == 55U ? 4U : 2U; /* CMD_TFLASH=55 включает два октета результата; ACK INITLOAD/LOAD содержит адрес и команду. */
frame[0] = s->address; frame[1] = command; frame[2] = status & 255U; frame[3] = status >> 8; /* Сохраняет принятый адрес и little-endian статус. */
for (i = 0U; i < size; ++i) crc = crc16(crc, frame[i]); /* Контрольная сумма защищает все значащие поля ответа. */
frame[size] = crc & 255U; frame[size + 1U] = crc >> 8; /* CRC16 передаётся младшим октетом вперёд. */
return s->port.transmit(s->port.user, frame, size + 2U); /* Reset разрешается только после фактического завершения передачи. */
} /* Ошибка UART никогда не превращается в разрешение перезапуска. */
void f28335_rs_abort(f28335_rs_boot_t *s) /* Инвалидирует частичный кадр и прежнее подтверждение образа. */
{ /* Порт вызывает эту функцию при межбайтовом таймауте, переполнении либо RXERROR. */
s->position = 0UL; s->loaded = 0U; s->length = 0UL; s->image.valid = false; /* Новый TFLASH после сбоя требует полного нового INITLOAD/LOAD. */
} /* Прерывание сеанса не изменяет действующую Flash. */
void f28335_rs_init(f28335_rs_boot_t *s, const f28335_rs_port_t *port, unsigned int address) /* Создаёт независимый экземпляр без скрытых глобальных переменных. */
{ /* Конфигурация платы должна передать все обязательные callbacks. */
memset(s, 0, sizeof(*s)); s->port = *port; s->address = address; /* Обнуляет кеш, счётчики и разрешение LOAD до первого принятого кадра. */
} /* Аппаратная инициализация SCI и GPIO остаётся в проекте платы. */
static void execute(f28335_rs_boot_t *s) /* Выполняет только полностью принятый запрос с подтверждённой CRC16. */
{ /* Транспортные поля никогда не разыменовываются как произвольные CPU-адреса. */
uint32_t source, destination, length; unsigned int status = 0U; pcan_boot_manifest_t manifest; /* Manifest общего валидатора используется только для размера и транспортной CRC32. */
if (s->command == 58U) { /* CMD_INITLOAD=58 резервирует фиксированный безопасный staging вместо произвольной записи памяти. */
source = get32(s->header + 2); length = get32(s->header + 6); s->loaded = 0U; s->length = 0UL; /* Новая подготовка всегда отзывает подтверждение прежнего блока. */
if (source == F28335_RS_STAGE && length >= 24UL && length <= F28335_RS_LIMIT && !(length & 1UL)) { s->length = length; reply(s, 58U, 0U); } /* Чётный SCI8 должен поместиться в предел общего проверяемого формата. */
} else if (s->command == 51U) { /* CMD_LOAD=51 уже принял поток в staging, но до этой точки не разрешал запись Flash. */
if (!s->fault && s->length) { s->loaded = 1U; reply(s, 51U, 0U); } /* Подтверждает блок только после полного приёма, успешной записи RAM и CRC16. */
} else if (s->command == 55U) { /* CMD_TFLASH=55 совместим с шестибайтовым статусом periph_28335. */
destination = get32(s->header + 2); source = get32(s->header + 6); length = get32(s->header + 10); /* Назначение, источник и размер сохраняют порядок полей существующего GUI. */
if (destination || source || length) { /* Нулевая тройка служит безопасной пробой поддержки без стирания и запуска. */
if (!s->loaded || destination != F28335_FLASH_APP_START || source != F28335_RS_STAGE || length != s->length) status = 0x1003U; /* Требует ровно тот блок, который подтвердил завершённый LOAD. */
else { /* Общий валидатор повторно читает RAM и сравнивает CRC32, записанную при приёме. */
memset(&manifest, 0, sizeof(manifest)); manifest.image_size = length; manifest.image_crc32 = s->crc32; /* Не доверяет CRC из непроверенного пользовательского manifest. */
if (!f28335_flash_update_validate(&s->image, &s->port.stage, 0UL, &manifest)) status = 0x1002U; /* Отвергает сектор A, CSM, OTP, перекрытия, неверный FAB1 и повреждённые данные до erase. */
else status = s->port.program(s->port.user, &s->image); /* Сохраняет конкретный код TI, защиту CSM 1004 и ошибку readback 1006; отказ оставляет RS recovery доступным. */
} /* Завершает проверку и аппаратную запись текущего образа. */
s->loaded = 0U; /* Повторная команда записи требует нового подтверждённого LOAD независимо от результата. */
} /* Безопасная проба не потребляет подтверждённый блок RAM. */
if (reply(s, 55U, status) && !status && length) s->port.reboot(s->port.user); /* Успешный reset следует только после записи, проверки физической Flash и отправки ACK. */
} /* Остальные команды не поддерживаются и не меняют память. */
} /* EEPROM параметров не входит ни в один callback загрузчика. */
void f28335_rs_feed(f28335_rs_boot_t *s, unsigned int value) /* Разбирает запросы последовательно без большого приёмного массива. */
{ /* Один вызов обрабатывает ровно один октет; время межбайтового ожидания контролирует порт. */
uint32_t position = s->position; pcan_boot_byte_t octet = value & 255U; /* Маска исключает флаги SCI и старшие биты широкого char. */
if (!position) { /* Новый кадр начинается только с адреса этого устройства. */
if (octet != s->address || !s->address || s->address > 255U) return; /* Чужой адрес не получает ACK и не запускает запись RAM. */
s->crc16 = 0xFFFFU; s->wire_crc = 0U; s->fault = 0U; /* CRC16 каждого запроса начинается со значения FFFF протокола Modbus. */
} /* После первого октета автомат ожидает номер команды. */
if (position == 1UL) { /* Второй октет задаёт фиксированную либо потоковую длину кадра. */
s->command = octet; /* Сохраняет неизменный номер BIOS-команды. */
if (octet == 58U) s->expected = 12UL; /* INITLOAD содержит адрес/команду, два u32 и CRC16: 12 октетов. */
else if (octet == 55U) s->expected = 16UL; /* TFLASH содержит три u32 и занимает 16 октетов с CRC16. */
else if (octet == 51U && s->length) { s->expected = s->length + 4UL; s->loaded = 0U; s->crc32 = 0UL; } /* LOAD состоит из адреса/команды, точного SCI8 и CRC16. */
else { f28335_rs_abort(s); return; } /* Неизвестная команда не позволяет использовать оставшиеся байты как адрес CPU. */
} /* Только распознанный заголовок разрешает приём полезных данных. */
if (position < 2UL || s->command != 51U) { if (position < 16UL) s->header[position] = octet; } /* Фиксированные запросы сохраняются в ограниченном заголовке. */
else if (position < s->expected - 2UL) { /* Поток LOAD записывается в RAM, пока завершающая CRC16 ещё не подтверждена. */
if (!s->port.stage_write(s->port.user, position - 2UL, octet)) s->fault = 1U; /* Ошибка RAM запрещает подтверждение и последующий erase. */
s->crc32 = pcan_boot_crc32_update(s->crc32, &octet, 1U); /* Независимая CRC32 позволяет обнаружить изменение staging после LOAD. */
} /* Два завершающих октета CRC не попадают в staging образа. */
if (position < 2UL || position < s->expected - 2UL) s->crc16 = crc16(s->crc16, octet); /* CRC16 включает адрес, команду и всю полезную нагрузку. */
else if (position == s->expected - 2UL) s->wire_crc = octet; /* Первый октет контрольной суммы является младшим. */
else s->wire_crc |= (unsigned int)octet << 8; /* Второй октет завершает 16-битную CRC принятого кадра. */
s->position = position + 1UL; /* Продвигает курсор только после обработки текущего октета. */
if (position >= 1UL && s->position == s->expected) { /* Запрос исполняется ровно один раз после получения последнего октета. */
s->position = 0UL; /* Следующий адрес начнёт независимый кадр даже при ошибочной CRC текущего. */
if (s->wire_crc == s->crc16) execute(s); else f28335_rs_abort(s); /* Повреждённый кадр отзывает прежний LOAD и никогда не вызывает program. */
} /* Частичный кадр остаётся в ожидании следующего октета либо внешнего timeout. */
} /* Автомат не использует системные часы, регистры MCU или динамическую память. */
File diff suppressed because one or more lines are too long
+88
View File
@@ -0,0 +1,88 @@
"""Проверяет production-автомат RS и общий writer на модели сбоев без платы.""" # Не вызывает JTAG, COM и TI Flash API.
import ctypes # Передаёт октеты настоящему скомпилированному C-автомату.
import importlib.util # Использует общий подписыватель FAB1 для корректных тестовых образов.
import os # Добавляет runtime GCC только в окружение тестового процесса.
from pathlib import Path # Разрешает пути относительно установленной библиотеки templates.
import struct # Формирует стандартные поля запросов и адресные секции SCI8.
import subprocess # Собирает C-модель с production-исходниками GCC.
import tempfile # Удаляет только собственные временные DLL проверки.
import unittest # Каждый отказ выполняется независимо и получает отдельный результат.
LIBRARY = Path(__file__).resolve().parents[1]; TEMPLATES = LIBRARY.parents[1]; PORT = TEMPLATES / "c/protocan-boot/ports/tms320f28335" # Тест использует те же валидатор и writer, которые компилируются для платы.
spec = importlib.util.spec_from_file_location("boot_test_contract", TEMPLATES / "python/protocan/f28335_flash.py"); contract = importlib.util.module_from_spec(spec); spec.loader.exec_module(contract) # Эталон FAB1 берётся из действующей общей библиотеки.
def section(address, words): # Создаёт адресный блок TI без дополнительных правил обновления.
return struct.pack("<HHH", len(words), address >> 16, address & 65535) + struct.pack("<" + "H" * len(words), *words) # Длина считается словами, каждое слово little endian, адрес — старшим словом вперёд.
def image(): # Минимальный правильный образ содержит разрыв для проверки CRC стёртых промежутков.
entry = contract.APP_DATA; loaded = {entry: 0x1234, entry + 1: 0x5678, entry + 12: 0xAABB, entry + 13: 0xCCDD} # Промежуток между двумя секциями должен остаться FFFF после erase.
return b"\xaa\x08" + bytes(16) + struct.pack("<HH", entry >> 16, entry & 65535) + section(contract.APP_START, struct.unpack("<16H", contract.descriptor(entry, loaded))) + section(entry, [0x1234, 0x5678]) + section(entry + 12, [0xAABB, 0xCCDD]) + bytes(2) # Заголовок FAB1 передаётся первым, но writer обязан опубликовать его последним.
class BootTests(unittest.TestCase): # Проверяет наблюдаемое поведение полного обмена, а не отдельные строки реализации.
@classmethod # DLL компилируется один раз для всех сценариев отказа.
def setUpClass(cls): # Изолирует бинарный результат и зависимости GCC.
gcc = Path(os.environ.get("CC", "C:/msys64/mingw64/bin/gcc.exe")); os.environ["PATH"] = str(gcc.parent) + os.pathsep + os.environ["PATH"] # CC позволяет выбрать другой совместимый GCC без изменения теста.
cls.temporary = tempfile.TemporaryDirectory(prefix="f28335-rs-tests-"); dll = Path(cls.temporary.name) / "model.dll" # Все артефакты испытания находятся вне прошивки пользователя.
subprocess.run([str(gcc), "-shared", "-std=c99", "-Wall", "-Wextra", "-Werror", str(LIBRARY / "tests/model.c"), str(PORT / "f28335_flash_app.c"), str(PORT / "f28335_flash_update.c"), str(TEMPLATES / "c/protocan-boot/src/pcan_boot.c"), "-o", str(dll)], check=True) # Компилирует production parser, CRC, validator и writer без извлечения копий функций.
cls.native = ctypes.CDLL(str(dll)); cls.native.model_crc.argtypes = [ctypes.c_char_p, ctypes.c_uint]; cls.native.model_crc.restype = ctypes.c_uint; cls.native.model_value.restype = ctypes.c_uint # ABI содержит только явно заданные целые числа и октеты.
@classmethod # Windows требует освободить библиотеку до удаления DLL.
def tearDownClass(cls): # Закрывает только дескриптор, созданный этим тестом.
if os.name == "nt": # На Windows открытая DLL удерживает файл от удаления.
release = ctypes.WinDLL("kernel32").FreeLibrary; release.argtypes = [ctypes.c_void_p]; release.restype = ctypes.c_int; release(cls.native._handle) # Освобождает единственную тестовую DLL с полным размером указателя.
cls.temporary.cleanup() # Временные файлы не остаются в библиотеке или проекте ИБП.
def setUp(self): self.native.model_reset() # Стирает модель между тестами, сохраняя независимость сценариев.
def frame(self, command, payload=b"", address=16): # Формирует запросы совместимого BIOS для управления production-автоматом.
body = bytes((address, command)) + payload; return body + struct.pack("<H", self.native.model_crc(body, len(body))) # Повреждение CRC тестируется отдельно, а фиксированный probe-вектор проверяет численное значение.
def send(self, data): # Аппаратные границы чтений могут разделить кадр после любого октета.
for octet in data: self.native.model_feed(octet) # Никакая модель не собирает кадр за production-автомат.
return bytes(self.native.model_value(10 + i) for i in range(self.native.model_value(0))) # Получает фактический ACK/status, отправленный callback transmit.
def load(self, payload): # Выполняет стандартный INITLOAD/LOAD без TFLASH.
self.assertEqual(len(self.send(self.frame(58, struct.pack("<II", 0x200000, len(payload))))), 4) # Подготовка RAM требует точного короткого ACK.
self.assertEqual(len(self.send(self.frame(51, payload))), 4) # Правильный LOAD подтверждается только после полной CRC16.
def program(self, payload): return self.send(self.frame(55, struct.pack("<III", 0x300000, 0x200000, len(payload)))) # Адреса и порядок полей совпадают с program_request periph_28335.
def assert_protected(self): self.assertEqual(self.native.model_value(5), 0xA55A) # Постоянный сектор A должен сохраняться даже после отказа на любой стадии записи.
def test_probe_and_success(self): # Полный успешный обмен должен подтвердить физическую Flash до reset.
probe = self.frame(55, bytes(12)); self.assertEqual(probe.hex(), "1037000000000000000000000000f066") # Вектор независимо подтверждён setprotocol.dll через protocan.periph28335.with_crc и обнаруживает изменение wire-формата.
response = self.send(probe); self.assertEqual(response[:4], bytes((16, 55, 0, 0))); self.assertEqual(self.native.model_value(1), 0) # Probe ничего не стирает и не останавливает действующую программу.
payload = image(); self.load(payload); response = self.program(payload); self.assertEqual(response[:4], bytes((16, 55, 0, 0))) # Успешный TFLASH сообщает нулевой статус.
self.assertEqual(self.native.model_value(4), 1); self.assertEqual(self.native.model_value(3), 1); self.assert_protected() # Реальный валидатор принимает физическую Flash, и только затем запрошен reset.
def test_wrong_address_crc_and_timeout(self): # Чужие либо оборванные запросы не должны разрешать запись.
payload = image(); self.assertEqual(self.send(self.frame(58, struct.pack("<II", 0x200000, len(payload)), address=17)), b"") # Чужой адрес не получает подтверждения INITLOAD.
self.load(payload); self.send(self.frame(58, struct.pack("<II", 0x200000, len(payload)))[:5]); self.native.model_abort() # Таймаут отбрасывает недопринятый запрос и старое подтверждение LOAD.
self.assertEqual(self.program(payload)[2:4], b"\x03\x10"); self.assertEqual(self.native.model_value(1), 0) # Без нового LOAD статус 1003 возникает до erase.
self.load(payload); damaged = bytearray(self.frame(55, struct.pack("<III", 0x300000, 0x200000, len(payload)))); damaged[-1] ^= 1; self.assertEqual(self.send(damaged), b"") # Неверная CRC TFLASH не вызывает никакого ответа об успехе.
self.assertEqual(self.native.model_value(1), 0); self.assert_protected() # Ни timeout, ни повреждение запроса не затронули Flash.
def test_changed_ram_and_read_failure(self): # CRC32 staging повторно проверяется после успешного LOAD.
payload = image(); self.load(payload); self.native.model_corrupt(60); self.assertEqual(self.program(payload)[2:4], b"\x02\x10") # Изменение RAM не скрывается первоначальным успешным ACK LOAD.
self.assertEqual(self.native.model_value(1), 0); self.load(payload); self.native.model_fault(0, 0, 1); self.assertEqual(self.program(payload)[2:4], b"\x02\x10") # Ошибка чтения staging также запрещает первый erase.
self.assertEqual(self.native.model_value(1), 0); self.assert_protected() # Память приложения и boot остались прежними.
def test_forbidden_addresses_and_bad_image(self): # Проверяет все опасные границы Flash и неполную таблицу.
for address in (0x2FFFFF, 0x338000, 0x33FFF6, 0x33FFF8, 0x380400): # Включает RAM/границу приложения, boot, CSM и OTP.
with self.subTest(address=hex(address)): # Ошибка конкретного диапазона видна отдельно в отчёте.
self.native.model_reset(); payload = image()[:-2] + section(address, [1, 2]) + bytes(2); self.load(payload); self.assertEqual(self.program(payload)[2:4], b"\x02\x10") # Правильная CRC16 не разрешает опасный LOAD-адрес внутри SCI8.
self.assertEqual(self.native.model_value(1), 0); self.assert_protected() # Неверная структура отвергается раньше физического стирания.
for payload in (image()[:-2], image() + bytes(2), image()[:-2] + section(contract.APP_DATA, [1]) + bytes(2)): # Проверяет отсутствующий терминатор, хвост и перекрытие секций.
self.native.model_reset(); self.load(payload); self.assertEqual(self.program(payload)[2:4], b"\x02\x10"); self.assertEqual(self.native.model_value(1), 0) # Любое нарушение структуры запрещает erase.
def test_failure_at_each_write_and_retry(self): # Имитирует обрыв каждого этапа программирования и последующее восстановление.
for operation in (100, 1, 2, 3): # 100 обозначает erase; три program-порции — две секции данных и последний FAB1.
with self.subTest(operation=operation): # Все стадии проверяются независимо на свежей модели Flash.
self.native.model_reset(); payload = image(); self.load(payload); self.native.model_fault(operation, 0, 0); self.assertEqual(self.program(payload)[2:4], b"\x16\x00") # Отказ writer передаёт статус ошибки TI вместо успешного ACK.
self.assertEqual(self.native.model_value(4), 0); self.assertEqual(self.native.model_value(3), 0); self.assert_protected() # Частичный образ не запускается, reset не запрошен, постоянный boot сохранён.
self.native.model_fault(0, 0, 0); self.load(payload); self.assertEqual(self.program(payload)[2:4], bytes(2)); self.assertEqual(self.native.model_value(4), 1) # Новый полный LOAD восстанавливает приложение через тот же оставшийся boot.
def test_failed_ack_prevents_reset(self): # Потеря передачи результата не должна перезапускать контроллер неожиданно для клиента.
payload = image(); self.load(payload); self.native.model_fault(0, 1, 0); self.program(payload) # Запись проходит успешно, но transmit возвращает ошибку.
self.assertEqual(self.native.model_value(4), 1); self.assertEqual(self.native.model_value(3), 0); self.assert_protected() # Физическая Flash готова, однако boot остаётся в recovery до явного последующего reset.
if __name__ == "__main__": unittest.main(verbosity=2) # Самостоятельный запуск выводит результаты модели без утверждений о проверке платы.
+105
View File
@@ -0,0 +1,105 @@
"""Собирает постоянный RS boot и выпускает проверенное Flash-приложение CCS.""" # Все аппаратные пути и исходники передаются проектом платы.
import argparse # Разбирает режим полной сборки либо postbuild уже собранного приложения.
from datetime import datetime # Восстанавливает двенадцать слов встроенного паспорта текущего prebuild.
import importlib.util # Подключает существующие инструменты firmware-info и FAB1 без копирования алгоритмов.
import json # Читает проектные настройки и сохраняет отдельные manifest приложения и boot.
from pathlib import Path # Разрешает пути независимо от текущего каталога CCS.
import re # Разбирает стандартные относительные ссылки PARENT-N-PROJECT_LOC в Eclipse.
import shutil # Переносит только проверенный комплект из временного каталога.
import struct # Сопоставляет двенадцать 16-битных слов паспорта точным октетам COFF.
import subprocess # Запускает TI compiler/OFD/hex2000 без shell и без доступа к программатору.
import tempfile # Изолирует объекты и неполные выпуски текущей попытки.
import xml.etree.ElementTree as ET # Читает реальные linked resources и include paths выбранной конфигурации CCS.
TEMPLATES = Path(__file__).resolve().parents[3] # Каталог tools расположен в templates/c/f28335-rs-boot/tools.
LIBRARY = Path(__file__).resolve().parents[1] # Общие исходники и linker boot берутся из того же checkout templates.
FLASH = TEMPLATES / "c/protocan-boot/ports/tms320f28335" # Использует единые валидатор FAB1 и writer SCI8.
def module(name, path): # Загружает существующий инструмент по конкретному пути выбранной библиотеки.
spec = importlib.util.spec_from_file_location(name, path) # Избегает случайного импорта другого установленного protocan.
result = importlib.util.module_from_spec(spec); spec.loader.exec_module(result); return result # Ошибка общей зависимости останавливает выпуск.
identity = module("rs_boot_identity", TEMPLATES / "c/firmware-info/tools/identity.py") # Сохраняет общие manifest, SHA-256 и имена архивов.
contract = module("rs_boot_contract", TEMPLATES / "python/protocan/f28335_flash.py") # Сохраняет единый формат подписания и проверки FAB1.
def project_paths(args): # Получает фактические C/ASM и include paths из настроек проекта.
project = args.project.resolve(); sources = [] # Проектный каталог содержит .project/.cproject и generated отдельной конфигурации.
for link in ET.parse(project / ".project").findall(".//linkedResources/link"): # Заголовки, OBJ и старые linker-файлы не компилируются как исходники.
uri = link.findtext("locationURI", ""); prefix, separator, relative = uri.partition("/") # Eclipse хранит корень ссылки отдельно от относительного пути.
match = re.fullmatch(r"PARENT-(\d+)-PROJECT_LOC", prefix) # Поддерживает переносимые ссылки любого уровня, не зашивая репозиторий платы.
root = project.parents[int(match[1]) - 1] if match else project # PARENT-1 указывает непосредственно на родителя CCS-проекта.
if separator and (match or prefix == "PROJECT_LOC") and Path(relative).suffix.lower() in (".c", ".asm"): sources.append((root / relative).resolve()) # Сохраняет только реальные C/ASM linked resources.
sources.extend((project / "setgui_firmware_identity").glob("*.c")) # Подключённый ранее паспорт SETGUI остаётся частью приложения.
config = next(item for item in ET.parse(project / ".cproject").findall(".//configuration") if item.get("name") == args.configuration) # Выбирает именно Debug либо Release, запрошенный CCS.
option = next(item for item in config.findall(".//tool/option") if "compilerID.INCLUDE_PATH" in item.get("superClass", "")) # Сохраняет порядок поиска действующих заголовков платы.
includes = []; macros = {"${PROJECT_ROOT}": project, "${PROJECT_LOC}": project, "${CG_TOOL_ROOT}": args.compiler.resolve(), "${BuildDirectory}": args.build_dir.resolve()} # Разрешает только ожидаемые файловые макросы CCS.
for item in option.findall("listOptionValue"): # Каждая include-настройка применяется без shell expansion.
value = item.get("value", "") # Пустой путь не должен означать случайный текущий каталог.
for macro, replacement in macros.items(): value = value.replace(macro, str(replacement)) # Подставляет фактические абсолютные пути текущей конфигурации.
if not value or "${" in value: raise ValueError("Не разрешён include path CCS: " + value) # Отсутствующую зависимость требуется исправить до компиляции.
includes.append(Path(value)) # Сохраняет приоритет dev_sup перед дополнительными TI-заголовками.
return list(dict.fromkeys(sources)), includes # Не компилирует повторную ссылку на один исходник дважды.
def compile_image(args, sources, includes, linker, output, optimize=False, api=False): # Создаёт самостоятельный COFF с собственным runtime.
compiler = args.compiler.resolve(); support = args.support.resolve(); objects = [] # Объекты boot не смешиваются с объектами приложения.
with tempfile.TemporaryDirectory(prefix="objects-", dir=output.parent) as directory: # Не использует старые OBJ из Debug либо другой версии исходников.
for index, source in enumerate(sources): # Уникальный индекс исключает коллизии одинаковых имён модулей из разных каталогов.
obj = Path(directory) / (str(index) + "_" + source.stem + ".obj") # Каждый результат принадлежит только этой попытке сборки.
command = [str(compiler / "bin/cl2000.exe"), "-v28", "-ml", "-mt", "--float_support=fpu32", "--abi=coffabi", "--gen_func_subsections=on", "--define=F28335_RS_CPU_HZ=150000000UL"] # Архитектура и частота соответствуют F28335 платы ИБП; частота используется формулой TI Flash API.
if source.suffix.lower() == ".c": command.append("--c99") # C99 требуется общим bool/stdint, но не передаётся как режим ассемблера.
if optimize: command.append("-O2") # Оптимизация boot уменьшает постоянный сектор A; алгоритмы управления приложения сохраняют штатный режим без оптимизации.
command.extend("--include_path=" + str(path) for path in includes) # Пути передаются отдельными аргументами, включая пробелы Windows.
subprocess.run(command + ["--compile_only", "--output_file=" + str(obj), str(source)], check=True); objects.append(obj) # Ненулевой код TI останавливает сборку до линковки.
command = [str(compiler / "bin/cl2000.exe"), "-v28", "--float_support=fpu32", "--abi=coffabi", "-z", "--rom_model", "--heap_size=0", "--stack_size=0x400", "--warn_sections", "--search_path=" + str(compiler / "lib"), "--search_path=" + str(support / "source")] # Отдельный runtime сохраняет boot работоспособным при пустом приложении.
if api: command.append("--search_path=" + str(args.flash_api.resolve() / "lib")) # Библиотека TI берётся из локальной установки и не копируется в templates.
command.extend(str(path) for path in objects) # Линкер получает только свежие объекты этой попытки.
command.extend([str(linker), str(support / "source/DSP2833x_Headers_nonBIOS.cmd"), "-lrts2800_fpu32.lib", "--map_file=" + str(output.with_suffix(".map")), "--output_file=" + str(output)]) # Общие регистры и runtime согласованы с COFF/FPU32 действующего проекта.
if not api: command.append(str(support / "source/rts2800_fpu32_fast_supplement.lib")) # Приложение сохраняет ускоренные sincos/sqrt/div/atan2 в прежней ramfuncs.
subprocess.run(command, check=True) # Отсутствие Flash API или переполнение RAM/Flash запрещает выпуск.
def build_boot(args, output): # Компилирует постоянный сектор A с независимыми исходниками платы.
support = args.support.resolve(); port = LIBRARY / "ports/tms320f28335" # Общий аппаратный порт не содержит GPIO конкретного изделия.
sources = [args.board.resolve(), LIBRARY / "src/f28335_rs_boot.c", port / "flash_api.c", FLASH / "f28335_flash_app.c", FLASH / "f28335_flash_update.c", TEMPLATES / "c/protocan-boot/src/pcan_boot.c", support / "source/DSP2833x_GlobalVariableDefs.c", support / "source/init28335_func.c", support / "source/DSP2833x_CodeStartBranch.asm"] # UART/GPIO принадлежат только board.c; формат и writer повторно используются из templates.
includes = [support / "include", LIBRARY / "include", args.flash_api.resolve() / "include", args.compiler.resolve() / "include"] # Все зависимости TI должны существовать до компиляции.
compile_image(args, sources, includes, port / "boot.cmd", output, optimize=True, api=True) # Boot использует только FLASHA и выделенные RAMAPI/RAMFUNCS.
document = ET.fromstring(subprocess.check_output([str(args.compiler / "bin/ofd2000.exe"), "-x", str(output)])) # Проверяет фактические LOAD-секции, а не только текст linker-команды.
for section in document.findall(".//ti_coff/section"): # Учитывает также Flash LOAD копируемого в RAM кода API.
if section.findtext("raw_data") is None or section.findtext("copy") == "true": continue # Незагружаемые секции отладки не программируются.
start = int(section.findtext("virtual_addr", "0"), 0); size = int(section.findtext("raw_data_size", "0"), 0) // 2 # OFD хранит LOAD как virtual_addr, размер raw_data измеряет октетами.
if not (0x338000 <= start < start + size <= 0x33FF80 or start == 0x33FFF6 and size == 2): raise ValueError("Boot OUT содержит LOAD вне постоянного сектора A") # Запрещает приложение, RAM LOAD, CSM и OTP в установочном образе boot.
def publish(args): # Проверяет оба независимых OUT перед переносом нового выпуска в постоянный каталог.
project = args.project.resolve(); build = args.build_dir.resolve(); metadata_path = build / "generated/firmware_identity.json" # Каждый Debug/Release использует собственный паспорт prebuild.
metadata = json.loads(metadata_path.read_text(encoding="utf-8")); settings = json.loads((project / "firmware_identity.json").read_text(encoding="utf-8-sig")) # Не резервирует дополнительную версию после компиляции.
if any(metadata.get(field) != settings.get(field) for field in ("product", "target", "version", "flashing")): raise ValueError("Настройки изменились: повторите prebuild и сборку") # Не связывает старый OUT с новым продуктом, номером либо маршрутом прошивки.
output = build / "firmware"; output.mkdir(parents=True, exist_ok=True) # Все временные и постоянные результаты принадлежат выбранной конфигурации CCS.
with tempfile.TemporaryDirectory(prefix=".rs-release-", dir=build) as directory: # Частичная сборка никогда не заменяет предыдущий выпуск.
stage = Path(directory); app = stage / "flash" / (args.artifact + ".out"); app.parent.mkdir() # Flash OUT приложения хранится отдельно от boot.
shutil.copy2(build / (args.artifact + ".out"), app); contract.seal_coff(app, args.compiler / "bin/ofd2000.exe") # Заполняет только заранее выделенный FAB1 после проверки всех LOAD-адресов приложения.
document = ET.fromstring(subprocess.check_output([str(args.compiler / "bin/ofd2000.exe"), "-x", str(app)])); entry, loaded, sections = contract.coff_sections(document) # Содержимое извлекается официальным TI OFD из реального COFF.
built = datetime.fromisoformat(metadata["built_at_utc"]); build_id = metadata["build_id"].encode("ascii"); expected = (1, *metadata["version"], built.year, built.month << 8 | built.day, built.hour << 8 | built.minute, built.second, *(build_id[i] << 8 | build_id[i + 1] for i in range(0, 8, 2))) # Двенадцать слов повторяют публичный firmware_info_static контракт паспорта без изменения версии.
if sum(tuple(loaded.get(address + i) for i in range(12)) == expected for address in loaded) != 1: raise ValueError("OUT не содержит единственный паспорт текущего prebuild") # Старый объект, чужая версия либо дублированный паспорт не выпускаются.
serial = stage / "rs" / (args.artifact + "_flash_rs.bin"); serial.parent.mkdir() # RS-клиент выбирает только SCI8 приложения, исключая постоянный boot.
subprocess.run([str(args.compiler / "bin/hex2000.exe"), str(app), "-boot", "-sci8", "-b", "-o", str(serial), "-map", str(serial.with_suffix(".bin.map"))], check=True) # Официальный конвертор сохраняет адресные секции TI SCI8.
contract.parse_sci8(serial.read_bytes()); boot = stage / "boot" / (args.artifact + "_boot.out"); boot.parent.mkdir(); build_boot(args, boot) # Проверяет BIN независимо от OUT и создаёт отдельный проверенный установочный boot.
shutil.copy2(build / (args.artifact + ".map"), app.with_suffix(".map")) # Карта соответствует неизменным адресам приложения до заполнения заголовка.
for image, kind in ((app, "COFF-FLASH"), (serial, "SCI8-FLASH"), (boot, "COFF-RS-BOOT")): # Каждый тип получает собственный manifest и SHA-256 точных байтов.
specific = dict(metadata, execution="BOOTLOADER" if image == boot else "FLASH", image_format=kind, boot_contract="f28335-universal-v1", boot_protected=True) # Явно разделяет установку постоянного boot и обновление приложения.
if image == boot: specific["flashing"] = {"default_loader": "f28335-jtag", "profiles": {"f28335-jtag": {}}} # Постоянный загрузчик устанавливается через JTAG и никогда не выбирается для RS TFLASH.
current = stage / "manifest-source.json"; current.write_text(json.dumps(specific, ensure_ascii=False, indent=2), encoding="utf-8"); identity.seal(image, current); identity.verify(image, image.with_suffix(image.suffix + ".manifest.json")) # Все стороны используют существующие проверенные средства firmware-info.
(stage / "manifest-source.json").unlink(); shutil.copy2(metadata_path, stage / "firmware_identity.json") # В архив попадает один исходный паспорт текущей компиляции.
destination = output / identity.build_folder_name(metadata); shutil.copytree(stage, destination, dirs_exist_ok=True) # Сохраняет предыдущие архивы и переносит только полностью проверенный выпуск.
shutil.copy2(app, build / app.name) # Основной OUT CCS получает тот же FAB1, что опубликованные RS BIN и Flash OUT.
if settings.get("copy_to") and not args.no_export: identity.export_build(destination, (project / settings["copy_to"]).resolve(), "rs/" + serial.name) # Установленный проектный export проверяет BIN в папке rs и применяется после всех локальных проверок.
print("[firmware-build] RS boot package: " + str(destination)); return destination # Показывает точный постоянный каталог для JTAG и последующего RS-обновления.
def main(): # Одна точка входа используется проектной обёрткой и проверкой воспроизводимости.
parser = argparse.ArgumentParser(description=__doc__); parser.add_argument("command", choices=("build", "postbuild")) # build компилирует приложение; postbuild использует OUT, уже собранный CCS.
for name in ("project", "compiler", "build-dir", "support", "board", "app-linker", "flash-api"): parser.add_argument("--" + name, type=Path, required=True) # Все аппаратные исходники и установки выбирает проект-потребитель.
parser.add_argument("--artifact", required=True); parser.add_argument("--configuration", default="Debug", choices=("Debug", "Release")); parser.add_argument("--no-export", action="store_true") # Локальная проверка может отключить дополнительное копирование в GUI.
args = parser.parse_args(); args.compiler = args.compiler.resolve(); args.build_dir = args.build_dir.resolve(); args.build_dir.mkdir(parents=True, exist_ok=True) # Нормализует только ожидаемые пути без shell expansion.
if Path(args.artifact).name != args.artifact or any(char in args.artifact for char in '\\/:*?"<>|'): parser.error("artifact должен быть именем OUT без расширения") # Имя файла не может выйти за выбранный каталог выпуска.
if args.command == "build": # Ручной полный build полезен для проверки проекта вне запущенной IDE.
sources, includes = project_paths(args); compile_image(args, sources, includes, args.app_linker.resolve(), args.build_dir / (args.artifact + ".out")) # Использует действующие linked resources без ручного списка копий исходников.
publish(args) # Ненулевые коды компиляции, конверсии и проверки распространяются в CCS.
if __name__ == "__main__": main() # Импорт тестами не запускает компилятор и не изменяет рабочую версию.
@@ -0,0 +1,30 @@
"""Проверяет настоящий SCI8 TI против ограничений штатного загрузчика UlitCo.""" # Проверка не открывает COM-порт и не изменяет Flash платы.
from pathlib import Path # Находит общий сборщик и установленный TI compiler относительно теста и стандартных каталогов.
import os # Позволяет CI явно выбрать установленный TI compiler без путей конкретного пользователя.
import struct # Читает эталонный заголовок SCI8 и два слова команды перехода C28x.
import subprocess # Получает фактическую точку входа COFF официальным инструментом TI OFD.
import sys # Подключает ту же реализацию build_kernel, которую использует postbuild CCS.
import tempfile # Каждая компиляция создаёт независимые файлы SDK и результаты.
import unittest # Запускает аппаратно независимую проверку стандартной библиотекой Python.
import xml.etree.ElementTree as ET # Извлекает адрес _InitBoot из официального описания исполняемого COFF.
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools')) # В рабочем templates сборщик находится рядом с каталогом tests.
import c2000_sci_build as sci # Компилирует настоящее RAM-ядро без подмены TI compiler и hex2000.
class SciKernelBuildTests(unittest.TestCase): # Фиксирует несовместимость entry=0x9000 со штатным primary_bootloader.obj.
def test_kernel_enters_at_zero_and_branches_to_initboot(self): # Проверяет транспортный вход и реальное назначение машинного перехода.
compiler = Path(os.environ.get('FIRMWARE_C2000_COMPILER', 'C:/ti/ccs1230/ccs/tools/compiler/ti-cgt-c2000_22.6.0.LTS')) # Переменная переопределяет стандартную установку TI на машинах CI.
if not (compiler / 'bin/cl2000.exe').is_file(): self.skipTest('Не установлен TI C2000 compiler') # Хостовые проверки без проприетарного toolchain остаются доступными.
try: ware = sci.find_c2000ware() # Использует тот же поиск SDK F28335, что штатная сборка проекта.
except ValueError as error: self.skipTest(str(error)) # Отсутствующий SDK явно помечает проверку как пропущенную.
with tempfile.TemporaryDirectory(prefix='ulitco-sci-entry-') as temporary: # Компиляция не изменяет исходники установленного SDK.
image = sci.build_kernel(compiler, Path(temporary) / 'f28335_sci_kernel.bin', 30000000, 0x0080, ware) # Плата использует кварц 30 МГц; маска 0x80 выбирает сектор H из комплекта, вызвавшего ошибку.
data = image.read_bytes() # Проверяет конечный транспортный BIN после настоящего hex2000.
self.assertEqual(data[:2], b'\xAA\x08') # Действующий приёмник ожидает ключ SCI8 0x08AA в порядке младшего байта вперёд.
self.assertEqual(struct.unpack_from('<HH', data, 18), (0, 0), 'primary_bootloader.obj отклоняет ненулевой адрес входа') # Восемь резервных слов после ключа оставляют entry на смещении 18 октетов.
self.assertEqual(struct.unpack_from('<HHH', data, 22), (2, 0, 0)) # Первый блок должен загрузить двухсловный переход по словному адресу 0x000000.
document = ET.fromstring(subprocess.check_output([str(compiler / 'bin/ofd2000.exe'), '-x', str(image.with_suffix('.out'))])) # Извлекает исходную точку входа COFF до её переопределения транспортным заголовком SCI8.
entry = int(document.findtext('.//optional_file_header/entry_point'), 0) # Сборщик линкует COFF с символом _InitBoot, который подготавливает стек и _ExitBoot.
self.assertGreater(entry, 0) # Нулевой codestart должен перенаправлять исполнение в отдельно размещённую функцию TI.
self.assertEqual(struct.unpack_from('<HH', data, 28), (0x0040 | (entry >> 16), entry & 0xFFFF)) # Кодирование LB C28x содержит старшие шесть бит адреса в первом слове и младшие 16 во втором; переход должен вести именно в _InitBoot.
if __name__ == '__main__': unittest.main() # Позволяет выполнить проверку отдельно от полного набора тестов библиотеки.
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: # Временный файл на том же диске позволяет атомарно заменить настройки без частичной записи.
@@ -94,6 +100,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 и TI Flash Kernel A для F28335 во всех портах.
src/firmware_image.c
src/altera_logic.c
src/altera_stream.c
@@ -100,6 +101,9 @@ endif()
option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS)
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) # Host-проверка использует ту же реализацию, что ctypes и будущие порты.
add_test(NAME c2000_sci COMMAND test_c2000_sci) # Делает контракт штатного загрузчика F28335 частью обычного 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.
"deif_xdi.c", # Общий DEIF входит в host-библиотеку.
"set_invertor_gas.c", # Собирает общий адаптер двух GAS для STM Invertor при прямом запуске host-builder.
"set_deif_gauge.c", "set_igbt_realtime.c",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+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.
+126
View File
@@ -1,5 +1,131 @@
# Подключение precompile и postcompile из templates
## Параметры прошивки CAN и RS из JSON
Настройка CCS/Keil в SETGUI теперь проверяет Python до изменения проекта.
Общие BAT вызывают `python-runtime.ps1`: он находит Python 3.8+ в установленном
окружении либо готовит проверенный переносимый runtime в
`%LOCALAPPDATA%/SET/FirmwareBuild`. Ручная настройка PATH не требуется.
Для Windows 10/11 x64 используется закреплённый Python 3.13.12, для Windows 7
и x86 — Python 3.8.10. ZIP загружается с python.org с проверкой SHA-256;
повторные сборки используют готовую локальную копию. При отсутствии доступа
к сети можно заранее установить Python либо задать полный путь `FIRMWARE_PYTHON`.
Для **штатного SCI-загрузчика UlitCo / C2Prog** выберите в настройке проекта
маршрут `f28335-sci`. Он автоматически подключает общую реализацию из
`c/firmware-info/tools/c2000_sci_build.py`; отдельный проектный `build_flash.py`
не требуется. Пример блока:
```json
"flashing": {
"default_loader": "f28335-sci",
"profiles": {
"f28335-sci": {
"baudrate": 115200,
"oscillator_hz": 30000000,
"enter_monitor": false,
"timeout_ms": 3000
}
}
}
```
Укажите фактическую частоту кварца: поддержаны 20, 25 и 30 МГц. Она определяет
времена Flash API и сохраняется вместе с RAM-ядром. Установленный C2000Ware с
`device_support/f2833x/examples/f28335_flash_kernel` определяется автоматически
в `C:/ti` либо по `C2000WARE_ROOT`. Эта зависимость проверяется при применении
настройки проекта. TI compiler берётся из `${CG_TOOL_ROOT}` текущей CCS.
Postbuild проверяет Flash LOAD-секции исходного OUT и выпускает
`*_flash_rs.bin`, `*_flash.out`, `f28335_sci_kernel.bin`, manifest, SHA-256
и уведомление о лицензии TI. Ядро собирается из штатных исходников SDK под
частоту платы и маску секторов именно текущего приложения. Основной manifest
в `latest_build.json` указывает сразу на Flash SCI BIN; папка `eeprom` для
этого маршрута не создаётся.
В SETGUI маршрут работает внутри общей вкладки прошивки: первичный загрузчик
принимает RAM-ядро TI с побайтовым эхом, затем Kernel A стирает выбранные сектора
и принимает порции Flash с подтверждением checksum после Flash API. Внешний
C2Prog не запускается. Тайм-аут, несовпадение эха/суммы и отмена завершают
операцию; readback-проверка памяти этим протоколом Kernel A не заявляется.
Ядро автоматически выбирается из manifest и проверяется по SHA-256.
`enter_monitor=true` включает штатную команду UlitCo: запись `1` в параметр
Monitor №93 перед autobaud. Действующая прошивка разрешает такой переход только
в остановленном состоянии. При `false` предварительно переведите плату в
первичный SCI-загрузчик обычным способом. Это отдельный маршрут относительно
`f28335-flash-rs`, который использует BIOS ПМ35 и защищённое приложение FAB1.
В проектном `firmware_identity.json` можно хранить блок `flashing` вместе с
версией. Keil и CCS проверяют его до увеличения номера сборки и переносят в
`generated/firmware_identity.json`, каждый `*.manifest.json` и архив выпуска.
Параметры описывают **загрузку образа**; скорость работающей программы на MCU
по-прежнему задаётся её исходниками и настройками аппаратуры.
```json
"flashing": {
"default_loader": "set-can",
"profiles": {
"set-can": {"bitrate": 250000, "node_id": 13},
"set-rs485": {"baudrate": 115200}
}
}
```
`default_loader` — конкретный загрузчик, выбираемый при импорте сборки или
локального образа; его ключ должен присутствовать в `profiles`. Каждая запись
`profiles` задаёт независимые параметры соответствующего загрузчика. Значения
примеров нужно заменить параметрами своей платы. JSON не допускает комментарии;
назначение полей приведено ниже.
| Загрузчик | Поддерживаемые параметры |
|---|---|
| `set-can` | `bitrate`, `node_id`, `product_type`, `hardware_min`, `hardware_max`, `base_address` |
| `protocan-can` | Поля `set-can`, кроме `base_address`; дополнительно `device_type`, `session_id`, `memory` |
| `set-rs485`, `stm32-uart` | `baudrate`, `base_address` |
| `f28335-eeprom`, `f28335-flash-rs` | `baudrate`, `controller`, `timeout_ms` |
| `stm32-usb` | `base_address` |
| `manual`, `f28335-jtag` | Пустой объект `{}` |
| Поле | Единицы и ограничения |
|---|---|
| `bitrate` | Целое число бит/с, `1..1000000`; верхняя граница Classic CAN. Например, `250000` — 250 кбит/с. Адаптер также должен поддерживать выбранную скорость. |
| `baudrate` | Целое число бод, `1..2147483647`; фактическую поддержку проверяет COM-драйвер. Например, `115200`. |
| `node_id` | Адрес SETP `0..255`; для ProtoCAN — поле Device `0..15`. |
| `device_type`, `session_id` | Поля существующего ProtoCAN-загрузчика: Type `0..7`, Session ID `1..255`. |
| `product_type` | Код совместимости `0..65535`; например, `358` соответствует `0x0166`, а `10291` — `0x2833`. |
| `hardware_min`, `hardware_max` | Включительный диапазон аппаратных ревизий `0..255`; min не больше max. |
| `base_address` | Целочисленный адрес приложения в **байтах**, `0..4294967295`. Например, `134230016` соответствует `0x08003000`; это адрес примера, а не общий адрес всех STM32. Для HEX должен совпадать с адресом самого образа. |
| `controller` | Независимый адрес контроллера RS-протокола ПМ35, `0..255`. |
| `timeout_ms` | Дополнительный тайм-аут RS-панели ПМ35 в миллисекундах, `50..10000`. |
| `memory` | Выбор назначения ProtoCAN: `eeprom`, `flash` или `stm32`. |
Формат UART определяется протоколом: SET RS использует 8N1, STM32 ROM Boot —
8E1. COM-порт, экземпляр CAN-адаптера и его соединение выбираются на компьютере
оператора, поскольку они не являются свойствами образа прошивки.
Обновлённый SETGUI выбирает загрузчик и заполняет форму из manifest при импорте
postbuild и выборе опубликованного комплекта. Общая вкладка прошивки также
подхватывает настройки при обычном выборе локального BIN/HEX, если рядом лежит
соответствующий `имя.bin.manifest.json` или `имя.hex.manifest.json`; перед
применением проверяются имя, размер и SHA-256 файла. Маршрут выбранной записи
каталога использует свой профиль, даже если `default_loader` задаёт другой.
При переключении общей формы между CAN и RS применяется профиль нового режима.
Локальный выбор BIN в панели F28335 подхватывает профиль выбранного RS-загрузчика
после штатной проверки образа; JTAG сохраняет отдельное подключение.
Скорость уже открытого канала применяется после его отключения. Запись платы
запускается отдельной штатной кнопкой.
Для отдельных CAN BIN и Flash RS BIN C2000 назначение из паспорта артефакта
имеет приоритет над `default_loader` основного EEPROM-образа. Профили при этом
остаются общими для комплекта. Старые конфигурации без `flashing` сохраняют
прежнее поведение; существующие проектные JSON автоматически не заменяются.
Полные примеры: [Keil](keil/firmware_identity.example.json) и
[CCS](ccs/firmware_identity.example.json). Поддержка чтения профилей должна быть
включена в сборку SETGUI вместе с обновлённым модулем
`set_devices.firmware_flashing` из templates.
Для проектов ПЛИС используйте [шаблон Xilinx ISE, Vivado и Altera/Intel Quartus](fpga/README.md):
общий сохраняемый итератор, паспорт на Verilog/VHDL, полная сборка `.bit`/`.sof`,
manifest, SHA-256 и архив выпуска.
@@ -2,5 +2,36 @@
"product": "MY_CONTROLLER",
"target": "TMS320F28335",
"version": [0, 1, 0],
"increment_version": true
"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
},
"f28335-sci": {
"baudrate": 115200,
"oscillator_hz": 30000000,
"enter_monitor": false,
"timeout_ms": 3000
},
"protocan-can": {
"bitrate": 250000,
"device_type": 1,
"node_id": 8,
"session_id": 1,
"product_type": 10291,
"hardware_min": 1,
"hardware_max": 1,
"memory": "eeprom"
}
}
}
}
+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 обслуживает postbuild и экспорт текущего проверенного выпуска.
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 Локальный выпуск использует ту же проверенную установку Python, что prebuild.
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 Общая обвязка находит Python 3.8+ или готовит локальный runtime до резервирования номера сборки.
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 Печать паспорта выполняется найденным или автоматически подготовленным Python.
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.exe в PATH.
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 Экспорт архива получает те же аргументы и найденный интерпретатор, что остальные стадии.
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 Архивирование без дополнительной папки также не зависит от глобальной настройки Python.
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 Подготовка Python выполняется общей обвязкой без зависимости от PATH запущенного Keil.
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 и инструментов прошивки.