14 Commits
Author SHA1 Message Date
Andrey 28fe22439c Объединить инструменты устройств и загрузчик F28335 в основной ветке 2026-11-13 19:32:52 +03:00
Andrey 26f9d62896 Согласовать SCI-ядро и жизненный цикл прошивки с UlitCo
Сохранить совместимый переход RAM-ядра, постоянный поток Qt,
точную диагностику обмена и журнал без повторов процентов.
Добавить проверку транспортной точки входа официальным TI compiler.
2026-11-13 19:23:41 +03:00
Andrey a197437985 Исправить ядро SCI UlitCo и отображение хода прошивки
Загружать RAM-ядро через переход по адресу 0 в _InitBoot,
повторно использовать поток Qt и передавать сигналы в GUI.
Показывать точный этап ошибки, изменения процентов и время сеанса.

Проверено: реальная сборка ядра TI и аппаратная передача ИБП
на скоростях 57600 и 115200 бод.
2026-11-13 19:23:40 +03:00
Andrey 4d101302be Добавить SCI и J-Link и обновить общие порты устройств
Подключить профили прошивки из JSON и подготовку Python для CCS/Keil.
Добавить SCI-программатор F28335 и поддержку STM32 через J-Link.
Обновить Linux-консоль, CAN485 и Modbus и проверки общих портов.
2026-11-13 17:07:20 +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
Andrey 07929c880a feat(set-protocol): добавь профиль CAN УКСВЭП 2026-11-12 15:53:12 +03:00
Andrey 4f7f6cdb87 Объединить инструменты устройств с актуальной основной веткой 2026-11-12 15:00:41 +03:00
Andrey 1fc88684d5 Расширить DSView осциллографом и декодерами двигателя и сигма-дельта 2026-11-12 15:00:41 +03:00
Andrey fa5ea166a9 Добавить шлюзы CAN/RS485 и Modbus и уточнить обработку CAN 2026-11-12 15:00:22 +03:00
Andrey 062f064c85 Сохранить сведения о проекте и совместимость сборки с Python 3.7 2026-11-12 15:00:06 +03:00
Andrey 7b1a16d6e4 Исправить сборку CCS и Keil на Python 3.7
Убрать несовместимый параметр missing_ok при очистке временных файлов.
Обеспечить повторное копирование архивов без параметра dirs_exist_ok.
Добавить проверки генерации заголовков и экспорта с API Python 3.7.
2026-11-10 15:13:35 +03:00
97 changed files with 6691 additions and 234 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/
-7
View File
@@ -60,13 +60,6 @@ USB и RS485 должны использовать разные tty. RTS/CTS в
физического трансивера с аппаратным управлением направлением либо предварительной
настройки драйвера UART; Linux-порт не переключает произвольные GPIO платы.
Для штатной USB-UART Linux-консоли Qt-порт предоставляет
`Can485Port.start_console_tunnel(port=37579)`: запускает `nc` из уже открытой
root shell к той же службе. Готовность определяется реальным `#INFO`, а не
открытием COM. При закрытии Ctrl-C завершает собственный nc, в пределах
400 мс проверяется возврат shell prompt и выполнение `stty sane`.
Поле `console_restored` позволяет аппаратному тесту проверить этот результат.
SD — заранее выбранный каталог `--logs`, например `/mnt/sd/can-logs`.
Журналы `can_*.csv` содержат совместимые текстовые строки кадров; импорт SETGUI
распознаёт этот формат автоматически. Выдача файла асинхронная, блоками 128 байт,
+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
+16
View File
@@ -39,6 +39,22 @@ class IdentityTests(unittest.TestCase):
self.assertEqual(self.prepare([clean,dirty])["build_id"], "abcdef0+")
self.assertEqual(self.prepare([dirty,clean])["build_id"], "abcdef0+")
def test_project_and_repository_survive_sealing(self): # Проверяет сведения, нужные Публикатору после переноса сборки на другой компьютер.
for project_name in (None, "Бальзам — ПМ35"): # Поддерживает прежние конфигурации и явно заданное русское название.
settings = {"product": "PM35", "target": "TMS320F28335", "version": [1, 2, 3]} # Код изделия сохраняет техническое назначение независимо от названия проекта.
if project_name is not None: # Отсутствующее поле должно использовать совместимое значение по умолчанию.
settings["project"] = project_name # Название из конфигурации переносится в паспорт без сокращений.
self.config.write_text(json.dumps(settings), encoding="utf-8") # Конфигурация сохраняется в той же кодировке, что и проект пользователя.
clean = {"commit": "a" * 40, "dirty": False} # Источник и templates остаются известными чистыми состояниями.
metadata = self.prepare([dict(clean), dict(clean)]) # Генерирует настоящий precompile JSON и заголовок без запуска компилятора.
self.assertEqual(metadata["project"], project_name or "PM35") # Старый JSON получает код изделия, новый — заданное название.
self.assertEqual(metadata["source"]["repository"], str(self.root.resolve())) # Абсолютный путь сохраняется в состоянии исходного проекта.
image = self.root / "controller.bin" # Отдельный образ позволяет проверить последующий postbuild manifest.
image.write_bytes(b"firmware fixture") # Небольшой файл достаточен для проверки сохранения полей и SHA-256.
sealed = identity.seal(image, self.root / "firmware_identity.json") # Штатный postbuild дополняет сохранённое происхождение без чтения нового HEAD.
self.assertEqual(sealed["project"], metadata["project"]) # Название доходит до того manifest, который импортирует GUI.
self.assertEqual(sealed["source"], metadata["source"]) # Путь, полный коммит и признак dirty переживают запечатывание образа.
def test_missing_git_is_explicit(self):
unknown = {"commit":None,"dirty":None}
self.assertEqual(self.prepare([unknown,unknown])["build_id"], "NOGIT000")
@@ -0,0 +1,66 @@
"""Проверки CCS/Keil с интерфейсами pathlib и shutil, доступными в Python 3.7.""" # Воспроизводит ограничения старого Python без установки другого интерпретатора.
import importlib.util # Загружает проверяемые инструменты именно из этой библиотеки templates.
import json # Проверяет сохранённый счётчик и фактический паспорт компиляции.
from pathlib import Path # Моделирует прежнюю сигнатуру удаления временных файлов.
import shutil # Моделирует прежнюю сигнатуру копирования каталогов.
import tempfile # Ограничивает запись изолированным каталогом теста.
import unittest # Проверки запускаются стандартным Python без сторонних зависимостей.
from unittest.mock import patch # Подменяет только несовместимые сигнатуры стандартной библиотеки.
def load_tool(name): # Импортирует одну точку входа без запуска CLI.
spec = importlib.util.spec_from_file_location("compatibility_" + name, Path(__file__).resolve().parents[1] / "tools" / (name + ".py")) # Выбор не зависит от PATH компьютера сборки.
module = importlib.util.module_from_spec(spec) # Отдельный экземпляр сохраняет изоляцию других тестов.
spec.loader.exec_module(module) # Загружает публичные функции подготовки и копирования.
return module # Тесты вызывают те же функции, что BAT-обвязки двух IDE.
identity, c2000 = load_tool("identity"), load_tool("c2000_build") # Обе цепочки используют общий формат паспорта.
original_unlink, original_copytree = Path.unlink, shutil.copytree # Сохраняет настоящие реализации перед ограничением их сигнатур.
def unlink37(path): # Python 3.7 не допускает аргумент missing_ok.
return original_unlink(path) # Реальное удаление сохраняет настоящие ошибки доступа и отсутствующего файла.
def copytree37(source, destination, symlinks=False, ignore=None, copy_function=shutil.copy2, ignore_dangling_symlinks=False): # Сигнатура Python 3.7 не допускает dirs_exist_ok.
return original_copytree(source, destination, symlinks, ignore, copy_function, ignore_dangling_symlinks) # Старый интерфейс продолжает копировать обычные новые каталоги.
class Python37CompatibilityTests(unittest.TestCase): # Проверяет успешный prebuild и повторный экспорт вместо одного синтаксического анализа.
def setUp(self): # Каждый сценарий начинает с отдельного проекта со включённым итератором версии.
temporary = tempfile.TemporaryDirectory(prefix="firmware python37 ") # Пробелы в пути соответствуют обычным пользовательским библиотекам.
self.addCleanup(temporary.cleanup) # Очистка выполняется после снятия ограничений стандартной библиотеки.
self.root = Path(temporary.name) # Рабочие проекты CCS/Keil не изменяются.
self.config = self.root / "firmware_identity.json" # Конфигурация принадлежит только временной проверке.
self.config.write_text(json.dumps({"product": "TEST_BOARD", "target": "TMS320F2812", "version": [1, 2, 3], "increment_version": True}), encoding="utf-8") # Этот режим воспроизводит исходную ошибку unlink после увеличения версии.
def test_keil_and_ccs_prepare_generate_headers_with_old_unlink(self): # Обе обвязки должны завершить генерацию при старой сигнатуре pathlib.
with patch.object(Path, "unlink", unlink37): # Любой оставшийся missing_ok вызовет тот же TypeError, что в журнале пользователя.
keil = identity.prepare(self.root, self.root, self.config, self.root / "Generated/target_0") # Keil напрямую вызывает общий prepare.
ccs = c2000.prepare(self.root, Path(__file__).resolve().parents[3], self.root / "STEND40_PCH_1/generated", config=self.config) # CCS загружает генератор из полной проверяемой библиотеки templates.
for relative, expected in (("Generated/target_0", [1, 2, 4]), ("STEND40_PCH_1/generated", [1, 2, 5])): # Две попытки резервируют разные номера без пропуска после TypeError.
output = self.root / relative # Проверяет фактический include-каталог каждой IDE.
self.assertIn("#define FIRMWARE_VERSION_PATCH " + str(expected[2]) + "U", (output / "firmware_identity.h").read_text(encoding="utf-8")) # Заголовок существует и содержит зарезервированную версию.
self.assertEqual(json.loads((output / "firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Паспорт совпадает с константами для компилятора.
self.assertEqual((keil["version"], ccs["version"]), ([1, 2, 4], [1, 2, 5])) # Обе публичные функции возвращают успешный результат.
def test_archive_export_and_atomic_copy_support_old_file_apis(self): # Проверяет повторное сохранение комплекта после успешного prebuild.
generated = self.root / "generated" # Метаданные сохраняются отдельно от исходного JSON.
identity.prepare(self.root, self.root, self.config, generated) # Получает настоящий паспорт для проверки SHA-256 и архива.
image = self.root / "application.bin" # Произвольные байты достаточны для общего Keil-контракта файловой целостности.
image.write_bytes(b"firmware bytes") # Проверяет сохранение точных байтов при нескольких копированиях.
with patch.object(Path, "unlink", unlink37), patch.object(shutil, "copytree", copytree37): # Воспроизводит оба ограничения Python 3.7 одновременно.
for attempt in range(2): # Второй экспорт должен объединять уже существующие каталоги.
result = identity.archive_build(image, generated / "firmware_identity.json", self.root / "archive", export=self.root / "export") # Печать папки соответствует настоящему postcompile Keil.
folder = Path(result["directory"]) # Использует точный архив текущего вызова.
(folder / "nested").mkdir(exist_ok=True) # Вложенный сопроводительный файл проверяет рекурсивное копирование.
(folder / "nested/map.txt").write_text("map " + str(attempt), encoding="utf-8") # Повторный выпуск обновляет существующий файл.
identity.copy_tree(folder, self.root / "ccs-output", copy_function=c2000.atomic_copy) # C2000 сохраняет атомарную замену с прежним интерфейсом unlink.
self.assertEqual((self.root / "ccs-output/nested/map.txt").read_text(encoding="utf-8"), "map 1") # Вложенные файлы обновились при повторном выпуске.
self.assertEqual((Path(result["exported_directory"]) / image.name).read_bytes(), image.read_bytes()) # Экспорт содержит исходный образ без изменения байтов.
self.assertFalse(list((self.root / "ccs-output").rglob(".firmware-*"))) # Успешная атомарная замена не оставляет незавершённых файлов.
if __name__ == "__main__": # Позволяет запустить регрессию стандартным unittest на компьютере сборки.
unittest.main() # Ненулевой результат выявляет несовместимость инструментов до настройки IDE.
+14 -2
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 для пользовательских заголовков конфигурации.
@@ -181,7 +182,10 @@ def atomic_copy(source, destination): # Сохраняет проверенны
shutil.copy2(source, temporary) # Сначала полностью записывает новые байты и сохраняет время исходного артефакта.
temporary.replace(destination) # Одной файловой операцией заменяет старый результат после успешной записи временного файла.
finally: # Незавершённый временный файл не должен попадать в будущий комплект Публикатора.
temporary.unlink(missing_ok=True) # Удаляет только собственный незавершённый файл; после успешной замены его уже нет.
try: # Python 3.7 не поддерживает параметр missing_ok у pathlib.Path.unlink.
temporary.unlink() # Удаляет только собственный незавершённый файл при ошибке копирования или замены.
except FileNotFoundError: # Успешный replace уже перенёс файл на постоянное имя.
pass # Отсутствующий временный файл не превращает успешное копирование в ошибку сборки.
return str(destination) # Сохраняет контракт copy_function стандартного shutil.copytree.
@@ -360,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 от постоянных каталогов выпуска.
@@ -386,7 +398,7 @@ def publish(project, templates, tools_templates, compiler, build_dir, artifact,
output.mkdir(parents=True, exist_ok=True) # Прежние проверенные выпуски сохраняются при добавлении нового.
for item in sorted(stage.iterdir(), key=lambda item: item.is_dir()): # Переносит только файлы и архив, созданные данным postcompile.
if item.is_dir(): # Архив содержит сопроводительные результаты той же сборки.
shutil.copytree(item, output / item.name, dirs_exist_ok=True, copy_function=atomic_copy) # Сохраняет только проверенный архив с атомарной заменой файлов при повторном выпуске той же компиляции.
identity.copy_tree(item, output / item.name, copy_function=atomic_copy) # Общий обход совместим с Python 3.7 и сохраняет атомарную замену каждого проверенного файла.
else: # Общие служебные файлы, если они появятся, сохраняются в корне текущего выпуска.
atomic_copy(item, output / item.name) # Не открывает старые файлы другой ветки на перезапись и не оставляет частичный BIN/HEX при отказе записи.
verify_package(output / image_name, metadata) # Подтверждает отсутствие повреждения при копировании на постоянный диск.
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.
+28 -3
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: # Временный файл на том же диске позволяет атомарно заменить настройки без частичной записи.
@@ -73,8 +79,13 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
try: stream.write(json.dumps(settings, ensure_ascii=False, indent=2) + "\n") # Сохраняет новый номер и все дополнительные поля проекта в UTF-8.
except BaseException: stream.close(); temporary.unlink(); raise # Ошибка записи оставляет исходный JSON целым и удаляет незавершённый файл.
try: temporary.replace(config) # Занимает номер до дальнейших шагов, поэтому отказ генерации или компиляции не повторит его.
finally: temporary.unlink(missing_ok=True) # Удаляет временный файл при ошибке замены; после успеха он уже переименован.
finally: # Очистка должна работать и в Python 3.7, который ещё не принимает missing_ok.
try: # После успешной замены временное имя уже отсутствует.
temporary.unlink() # Удаляет только собственный незавершённый файл при отказе replace.
except FileNotFoundError: # Отсутствие файла после успешного rename является штатным результатом.
pass # Другие ошибки файловой системы продолжают останавливать подготовку.
source, dependency = git_state(repository), git_state(templates)
source = dict(source, repository=str(repository.resolve())) # Сохраняет абсолютный локальный путь исходного проекта для Публикатора без адресов remote и учётных данных.
# A clean application built with a modified templates checkout is dirty too.
known = source["commit"] is not None and dependency["commit"] is not None
dirty = bool(source["dirty"] or dependency["dirty"])
@@ -86,10 +97,11 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
stamp = stamp.replace(microsecond=0)
if not 2000 <= stamp.year <= 2199:
raise ValueError("build year outside firmware-info v1 range")
metadata = {"schema": 1, "product": settings["product"], "target": settings["target"],
metadata = {"schema": 1, "project": settings.get("project") or settings["product"], "product": settings["product"], "target": settings["target"], # Необязательное название проекта переносится из конфигурации; старые проекты используют свой код изделия.
"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))))
@@ -152,6 +164,19 @@ def build_folder_name(metadata: dict) -> str:
return identity + "_" + stamp.strftime("%Y-%m-%d_%H-%M-%S_UTC+3")
def copy_tree(source: Path, destination: Path, copy_function=shutil.copy2): # Объединяет архив с существующим каталогом без dirs_exist_ok, отсутствующего в Python 3.7.
source, destination = Path(source), Path(destination) # Принимает пути обоих типов из общего экспорта и проверенной C2000-сборки.
destination.mkdir(parents=True, exist_ok=True) # Повторный экспорт сохраняет соседние файлы, но не принимает обычный файл вместо папки.
for entry in source.iterdir(): # Копирует только содержимое конкретного проверенного архива.
target = destination / entry.name # Сохраняет исходные имена и вложенную структуру комплекта.
if entry.is_dir(): # Вложенные каталоги требуют того же объединения при повторном выпуске.
copy_tree(entry, target, copy_function) # Передаёт атомарное копирование C2000 на все уровни архива.
else: # Каждый файл обрабатывается выбранным способом сохранения.
copy_function(entry, target) # По умолчанию сохраняет метаданные copy2, а C2000 использует atomic_copy.
shutil.copystat(source, destination) # Восстанавливает время и атрибуты каталога после записи дочерних файлов.
return str(destination) # Сохраняет возвращаемый тип стандартного shutil.copytree.
def export_build(folder: Path, export: Path, image_name: str) -> Path:
"""Copy the current complete build folder, without scanning old archives."""
folder, export = folder.resolve(), export.resolve()
@@ -161,7 +186,7 @@ def export_build(folder: Path, export: Path, image_name: str) -> Path:
verify(folder / image_name, folder / (image_name + ".manifest.json"))
export.mkdir(parents=True, exist_ok=True)
if destination != folder:
shutil.copytree(folder, destination, dirs_exist_ok=True)
copy_tree(folder, destination) # Повторное копирование архива совместимо с Python 3.7 без потери соседних выпусков.
verify(destination / image_name, destination / (image_name + ".manifest.json"))
print(f"[firmware-build] Exported folder: {destination}")
return destination
+5
View File
@@ -10,6 +10,7 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
# SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
set(SETPROTOCOL_V2_SOURCES
src/c2000_sci.c # Общий автомат SCI Boot F28335 входит во все host и MCU цели.
src/firmware_image.c
src/altera_logic.c
src/altera_stream.c
@@ -28,6 +29,7 @@ set(SETPROTOCOL_V2_SOURCES
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
set(SETPROTOCOL_LEGACY_SOURCES
src/uksvep_can.c # Общий профиль STM32 UKSVEP: маршруты, payload и карта дискретов.
src/balsam_can.c
src/set_crc.c
src/periph28335.c
@@ -104,6 +106,9 @@ if(SETP_BUILD_TESTS)
target_link_libraries(test_modbus_gateway PRIVATE setprotocol_static) # Проверяет тот же исходник, который выполняется на Zynq.
add_test(NAME modbus_gateway COMMAND test_modbus_gateway) # Делает проверку шлюза частью стандартного CTest.
enable_testing()
add_executable(test_c2000_sci tests/test_c2000_sci.c) # Эталонные байты ROM и TI Kernel A проверяются без устройства.
target_link_libraries(test_c2000_sci PRIVATE setprotocol_static) # Проверяет тот же C99-модуль, что использует ctypes.
add_test(NAME c2000_sci COMMAND test_c2000_sci) # Регистрирует контракт в обычном CTest.
add_executable(test_deif_xdi tests/test_deif_xdi.c) # Прямые проверки C-контракта DEIF.
target_link_libraries(test_deif_xdi PRIVATE setprotocol_static) # Используется единственное общее ядро.
add_test(NAME deif_xdi COMMAND test_deif_xdi "${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/deif-xdi-v1.tsv") # Векторы общие с JNI и Python.
+5 -4
View File
@@ -132,8 +132,9 @@ FFI-клиенты подключают `setprotocol_abi.h`. Имена функ
[Адресация GAS](docs/GAS_SLOT.md): `0x3` — отдельные 16-битные регистры `0…65535`, запись 1…4 слов. `0x4` — слот с Word0-маской и тремя значениями; PERIPH сохраняет 200 слов в 67 слотах `0xMM00…0xMM42`. Формат определяется MsgType, без настройки профиля.
## Modbus: шлюз USB / TCP
## УКСВЭП STM32F103RC
`set_modbus_gateway.h` содержит общий C99-кодек RTU ↔ TCP, проверку ответов
и формат таблицы маршрутов без зависимости от Linux и Qt. Контракт порта
и устройство слоёв описаны в [MODBUS_GATEWAY.md](../../doc/MODBUS_GATEWAY.md).
Общий профиль `uksvep_can.h/.c` предоставляет карту дискретов и параметров,
маршруты проекта «Ледокол», C-кодирование регистрового CAN и расчёт состояний
для эмулятора SETGUI. Контракт ABI, единицы и проверки:
[UKSVEP_CAN.md](docs/UKSVEP_CAN.md).
+64
View File
@@ -0,0 +1,64 @@
# UKSVEP STM32F103RC: регистровый CAN
Профиль относится к `UKSVEP_ICE_22220.6`: `Core/Src/can.c`,
`Core/Inc/package.h`, `Core/Src/message.c`, `Core/Src/lampa.c` и `main.h`.
Он подключён в SETGUI как отдельный эмулятор проекта «Ледокол».
Classic CAN, extended 29-битный ID, DLC=8, 250000 бит/с.
DeviceID 0…15 задают J0…J3; `Mode=DeviceID+1`.
Данные ВЭП: `0x001CE000 | DeviceID`; команды ВЭП:
`0x001CE020 | DeviceID`. Это регистровое приложение, а не ProtoCAN Boot Type5.
Broadcast из старого приложения не выдаётся этим клиентом за адресный ответ.
Payload общий с историческим `balsam_can`: word2, word3,
адрес/маска, word1, по два байта big endian. В заголовке биты15…13 задают
присутствие слов, биты12…0 — начальный адрес. `uksvep_can` ограничивает
банк R0…R127; отсутствующие слова не должны обнулять предыдущее состояние.
Маршрутизация других Balsam-плат остаётся в их собственном профиле.
| Регистры | Назначение |
|---|---|
| 0 / 1 / 2 | Входы / неисправности / аварии |
| 8 / 9 | Маски неисправностей / аварий |
| 23 | Перемычки; младший полубайт соответствует DeviceID |
| 48 | Sleep_time, секунды |
| 100 | Brightness, 0…10 тактов ШИМ |
| 101 / 102 | Cancount, быстрый / медленный период по 10 мс |
| 125 / 126 / 127 | Версия протокола / Mode / команды |
Назначения входов, GPIO, команд и значения по умолчанию доступны через
`uksvep_field_*`, `uksvep_input_name`, `uksvep_command_name`.
`uksvep_evaluate_inputs` моделирует ReadEnteres: R0.7 никогда не является
неисправностью/аварией; R0.8 всегда является неисправностью и становится
аварией по Error_mask.8; R0.9 всегда является неисправностью и аварией.
Вход R0.9/PB12 читается без инверсии, остальные назначенные
сигналы собираются в соответствии с физической картой STM32-проекта.
## Слои
`SETGUI → protocan/uksvep.py (ctypes) → uksvep_can.c → общий payload balsam_can.c`.
Ни GPIO, ни Qt, ни часы ОС общему C-профилю не нужны; динамическая память
не используется. Вычисления и wire не дублируются в Python.
Все выходные буферы принадлежат caller и имеют явную capacity:
- `uksvep_can_encode`: минимум8 байтов, возвращает8 либо0;
- `uksvep_can_decode`: минимум8 слов uint16 — направление, количество,
затем максимум3 пары адрес/значение; возвращает число пар,0 для чужого ID,
−1 для неверных аргументов/своего повреждённого кадра;
- `uksvep_field_info`: минимум4 слова uint16 — адрес, максимум, default, flags;
- `uksvep_evaluate_inputs`: минимум2 слова uint16 — неисправности и аварии.
Флаги полей: PARAM=1, COMMAND=2, STATE=4. GUI использует их для выбора
допустимых редакторов роли. Имена возвращаются постоянными строками UTF-8;
владение памятью клиенту не передаётся. Для применения RX клиент дополнительно
отбрасывает STD/RTR и собственное направление/эхо. Физические уровни
LED0…LED3 и PVT по текущему регистровому CAN не передаются; интерфейс не
должен представлять расчётные лампы как измерение этих GPIO.
## Проверки
`test_uksvep_can` фиксирует независимые wire-эталоны, все16 адресов,
малый выходной буфер, границы R127, sparse101, неверный DLC, чужие профили,
особые входы и регрессию прежнего decoder Бальзам. Python-тесты используют
ту же DLL, GUI-тесты подменяют только аппаратный драйвер.
+3
View File
@@ -43,6 +43,9 @@ typedef struct {
uint16_t values[BALSAM_CAN_REGISTER_COUNT];
} balsam_can_frame_t;
size_t balsam_can_encode_payload(uint16_t address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity); /* Общий payload word2/word3/адрес/word1 для исторических регистровых CAN-профилей. */
int balsam_can_decode_payload(const uint8_t *data, size_t size, balsam_can_frame_t *output); /* Разбирает только три слова и маску, оставляя маршрутизацию конкретному профилю. */
/** Return non-zero for the command/data/terminal IDs used by CAN_Bal_2812. */
int balsam_can_is_id(uint32_t can_id);
+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. */
+23
View File
@@ -0,0 +1,23 @@
#ifndef UKSVEP_CAN_H /* Защищает общий профиль ВЭП от повторного объявления. */
#define UKSVEP_CAN_H /* Карта соответствует STM32-проекту UKSVEP_ICE_22220.6, а не ВЭП Бальзам. */
#include "pcan_abi.h" /* Даёт экспортируемый C ABI для Python и других клиентов templates. */
#ifdef __cplusplus /* Позволяет использовать тот же профиль из C++ и JNI. */
extern "C" { /* Сохраняет стабильные имена экспортов без C++ mangling. */
#endif /* Завершает выбор языка связывания. */
#define UKSVEP_REGISTER_COUNT 128U /* message.h выделяет Modbus_LEN=0x80 слов uint16. */
#define UKSVEP_FIELD_PARAM 1U /* Поле допускает запись параметра в реальный ВЭП. */
#define UKSVEP_FIELD_COMMAND 2U /* Поле R127 содержит битовые команды приложения. */
#define UKSVEP_FIELD_STATE 4U /* Поле состояния доступно для редактирования только в роли эмулируемого ВЭП. */
PCAN_ABI_API uint32_t uksvep_can_id(unsigned device, unsigned from_device); /* Адрес 0…15: данные 0x1CE000+ID, команды 0x1CE020+ID; UINT32_MAX означает отказ. */
PCAN_ABI_API size_t uksvep_can_encode(unsigned device, unsigned from_device, unsigned address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity); /* Строит DLC=8 для одного–трёх слов; capacity задаётся в байтах, требуется минимум 8. */
PCAN_ABI_API int uksvep_can_decode(unsigned device, uint32_t id, const uint8_t *data, size_t size, uint16_t *output, size_t capacity); /* output: направление, число, три пары адрес/значение; capacity в словах uint16, минимум 8; 0=чужой ID, -1=ошибка. */
PCAN_ABI_API unsigned uksvep_field_count(void); /* Число подтверждённых package.h полей, без выдуманных регистров. */
PCAN_ABI_API int uksvep_field_info(unsigned index, unsigned device, uint16_t *output, size_t capacity); /* output[4]: адрес, максимум, начальное значение, флаги роли; capacity задаётся в словах uint16. */
PCAN_ABI_API const char *uksvep_field_name(unsigned index); /* Возвращает постоянное имя UTF-8 без передачи владения памятью. */
PCAN_ABI_API const char *uksvep_input_name(unsigned bit); /* Возвращает назначение и физический GPIO каждого из четырнадцати входов. */
PCAN_ABI_API const char *uksvep_command_name(unsigned bit); /* Описывает только команды package.h; пустая строка означает резерв. */
PCAN_ABI_API void uksvep_evaluate_inputs(uint16_t inputs, uint16_t alarm_mask, uint16_t error_mask, uint16_t *output, size_t capacity); /* Вычисляет R1/R2 по ReadEnteres; capacity задаётся в словах uint16, требуется минимум 2. */
#ifdef __cplusplus /* Закрывает внешнее C-связывание для C++. */
} /* Все клиенты вызывают одну реализацию протокола. */
#endif /* Завершает совместимость с C++. */
#endif /* Завершает публичный профиль UKSVEP. */
+35 -9
View File
@@ -48,11 +48,44 @@ int balsam_can_is_id(uint32_t can_id)
&& relative < BALSAM_CAN_DATA_OFFSET + BALSAM_CAN_NODE_COUNT);
}
size_t balsam_can_encode_payload(uint16_t address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity) /* Общий регистровый payload не зависит от выбранной платы. */
{ /* Формат совпадает у Бальзам и UKSVEP, но адреса CAN определяют разные профили. */
size_t index; /* Перебирает только присутствующие полезные слова. */
uint16_t header; /* Хранит трёхбитную маску и 13-битный адрес регистра. */
if (capacity < 8U || address > 0x1FFFU || values == NULL || output == NULL || count == 0U || count > 3U) { /* Проверяет геометрию payload до изменения выходных байтов. */
return 0U; /* Отказ не создаёт частично готового кадра. */
} /* Конкретный профиль дополнительно ограничивает размер своего банка. */
memset(output, 0, 8U); /* Отсутствующие слова получают нулевое заполнение без бита присутствия. */
header = (uint16_t)(address | (((7U << (3U - count)) & 7U) << 13U)); /* Одно/два/три слова дают маски 100/110/111 в битах 15…13. */
output[4] = (uint8_t)(header >> 8U); /* Старший байт адреса/маски передаётся первым. */
output[5] = (uint8_t)header; /* Младший байт завершает big-endian заголовок. */
for (index = 0U; index < count; ++index) { /* eCAN передаёт word2 и word3 перед word1. */
size_t offset = index == 0U ? 6U : (index - 1U) * 2U; /* Первое слово находится в байтах 6/7, следующие — в 0/1 и 2/3. */
output[offset] = (uint8_t)(values[index] >> 8U); /* Записывает старшие восемь бит без зависимости от endian хоста. */
output[offset + 1U] = (uint8_t)values[index]; /* Записывает младшие восемь бит того же слова. */
} /* Порядок слов реализован в одном C-модуле для всех потребителей. */
return 8U; /* Исторический регистровый кадр всегда имеет DLC=8. */
} /* Завершает общий кодировщик трёх регистров. */
int balsam_can_decode_payload(const uint8_t *data, size_t size, balsam_can_frame_t *output) /* Отделяет общий payload от профиля CAN ID. */
{ /* Маршрут и номер устройства заполняет вызывающий профиль. */
uint16_t header; /* Получает маску присутствия и начальный адрес слова. */
if (data == NULL || output == NULL || size != BALSAM_CAN_DLC) { /* Не читает неполные либо отсутствующие восемь байтов. */
return 0; /* Отказ не изменяет ранее принятые значения клиента. */
} /* Сохраняет исходный порядок декодирования Balsam без второго codec. */
header = get_be16(&data[4]); /* Адрес находится в третьем 16-битном слове на проводе. */
output->present_mask = (uint8_t)((header >> 13U) & 0x07U); /* Три старших бита разрешают отдельные регистры. */
output->start_address = (uint16_t)(header & 0x1FFFU); /* Остальные 13 бит задают начальный адрес. */
output->values[0] = get_be16(&data[6]); /* Первое логическое слово передаётся последним. */
output->values[1] = get_be16(&data[0]); /* Второе логическое слово передаётся первым. */
output->values[2] = get_be16(&data[2]); /* Третье логическое слово передаётся вторым. */
return 1; /* Все три значения разобраны; применять можно только присутствующие. */
} /* Завершает общий разбор payload без назначения платы. */
int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
balsam_can_frame_t *output)
{
uint32_t relative;
uint16_t header;
if ((data == NULL) || (output == NULL)) return -1;
can_id &= 0x1FFFFFFFUL;
if (!balsam_can_is_id(can_id)) return 0;
@@ -67,14 +100,7 @@ int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
output->direction = can_id == 0x00745019UL
? BALSAM_CAN_FROM_NODE : BALSAM_CAN_TO_NODE;
}
/* eCAN sends MDL before MDH: value2, value3, address/mask, value1. */
header = get_be16(&data[4]);
output->present_mask = (uint8_t)((header >> 13) & 0x07U);
output->start_address = (uint16_t)(header & 0x1FFFU);
output->values[0] = get_be16(&data[6]);
output->values[1] = get_be16(&data[0]);
output->values[2] = get_be16(&data[2]);
return 1;
return balsam_can_decode_payload(data, size, output); /* Переиспользует тот же payload decoder, сохраняя прежние проверки и маршрутизацию Бальзам. */
}
const char *balsam_can_device_name(uint8_t device)
+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 в первичный загрузчик. */
+143
View File
@@ -0,0 +1,143 @@
#include "uksvep_can.h" /* Объявляет общий формат, метаданные и ABI профиля UKSVEP. */
#include "balsam_can.h" /* Повторно использует исторический порядок трёх слов без второго кодека. */
#include <string.h> /* Обнуляет кадр до заполнения присутствующих слов. */
typedef struct { /* Хранит подтверждённые параметры регистра из package.h/message.c. */
uint16_t address; /* Индекс слова в modbus[128], без смещения Modbus 40001. */
uint16_t maximum; /* Верхняя граница ввода в исходных единицах прошивки. */
uint16_t initial; /* Значение для предпросмотра, не считается принятым от платы. */
uint16_t flags; /* Отделяет параметры, команды и эмулируемое состояние. */
const char *name; /* Постоянная подпись UTF-8 для всех потребителей профиля. */
} field_t; /* Структура остаётся внутренней: через ABI передаются скаляры. */
static const field_t fields[] = { /* Источник адресов — Core/Inc/package.h текущего STM32-проекта. */
{0U, 65535U, 0x247FU, UKSVEP_FIELD_STATE, "Дискретные входы"}, /* R0: нейтраль включает исправные питания, 380 В, ЛСУ и СКК; аварийные биты 8/9 выключены. */
{1U, 65535U, 0U, UKSVEP_FIELD_STATE, "Неисправности"}, /* R1 вычисляется из входов и Alarm_mask, а не является прямым аппаратным входом. */
{2U, 65535U, 0U, UKSVEP_FIELD_STATE, "Аварии"}, /* R2 вычисляется из входов и Error_mask. */
{8U, 65535U, 0x277FU, UKSVEP_FIELD_PARAM, "Маска неисправностей"}, /* Default_params: 0x3FFF без битов 7, 11 и 12. */
{9U, 65535U, 0x0200U, UKSVEP_FIELD_PARAM, "Маска аварий"}, /* По умолчанию выбран бит 9, авария сети 24 В. */
{23U, 65535U, 0U, UKSVEP_FIELD_STATE, "Перемычки J0…J3 / TEST"}, /* Jumpers=R0x17, младший байт задаёт физические перемычки. */
{48U, 65535U, 60U, UKSVEP_FIELD_PARAM, "Время до отключения, с"}, /* Millisecond умножает Sleep_time на 1000 мс. */
{100U, 10U, 9U, UKSVEP_FIELD_PARAM, "Яркость лампы, 0…10"}, /* ШИМ использует десять отсчётов, штатное значение Brightness=9. */
{101U, 65535U, 20U, UKSVEP_FIELD_PARAM, "Быстрый CAN, ×10 мс"}, /* Cancount[0]=20 соответствует 200 мс между пакетами. */
{102U, 65535U, 200U, UKSVEP_FIELD_PARAM, "Медленный CAN, ×10 мс"}, /* Cancount[1]=200 соответствует 2000 мс между пакетами. */
{125U, 65535U, 1U, UKSVEP_FIELD_STATE, "Версия прикладного протокола"}, /* PROTOKOL=1 относится к регистровому приложению, не к ProtoCAN Boot. */
{126U, 65535U, 1U, UKSVEP_FIELD_STATE, "Mode = DeviceID + 1"}, /* При запросе метаданных значение уточняется по выбранному адресу. */
{127U, 65535U, 0U, UKSVEP_FIELD_COMMAND, "Команды"} /* cTestLamp/cDefParam/cSaveParam/cLoadParam/cExtLamp/cExtLite/cReset. */
}; /* Все адреса и значения по умолчанию определены в одном общем модуле. */
static const char *const inputs[] = { /* Порядок битов повторяет ReadEnteres в Core/Src/lampa.c. */
"ИП1 · контроллер · PC13 · активный 0", /* Бит 0 читает инверсию IN_06. */
"ИП2 · периферия · PC15 · активный 0", /* Бит 1 читает инверсию IN_05. */
"ИП3 · замки / освещение · PA0 · активный 0", /* Бит 2 читает инверсию IN_04. */
"ИП4 · датчики + · PA1 · активный 0", /* Бит 3 читает инверсию IN_03. */
"ИП5 · датчики − · PA2 · активный 0", /* Бит 4 читает инверсию IN_02. */
"ИП6 · драйверы · PA3 · активный 0", /* Бит 5 читает инверсию IN_01. */
"Три фазы 380 В · PB15 · активный 0", /* Бит 6 читает инверсию IN_07. */
"Заряд накопителя · PB14 · активный 0", /* Бит 7 не становится неисправностью либо аварией. */
"Разряд накопителя · PB13 · активный 0", /* Бит 8 всегда становится неисправностью при единице. */
"Авария 24 В · PB12 · активный 1", /* Бит 9 единственный читается без инверсии IN_10. */
"Питание ЛСУ · PB1 · активный 0", /* Бит 10 читает инверсию IN_11. */
"Резерв 11 · PB0 · активный 0", /* Бит 11 читает инверсию IN_12. */
"Резерв 12 · PA5 · активный 0", /* Бит 12 читает инверсию IN_13. */
"Питание СКК · PA4 · активный 0" /* Бит 13 читает инверсию IN_14. */
}; /* Логическая единица здесь означает установленный бит R0, а не высокий GPIO. */
static const char *const commands[16] = { /* Имена битов берутся из package.h, без команд чужого задатчика. */
"Тест ламп", "Параметры по умолчанию", "Сохранить параметры", "Загрузить параметры", /* Биты 0…3 действительно обслуживаются main. */
"", "", "ExtLamp · флаг", "ExtLite · флаг", /* Биты 6/7 объявлены, но текущая логика лампы не использует их как отдельные команды. */
"", "", "", "", "", "", "", "Перезагрузка" /* Бит 15 запрещает продление IWDG и вызывает аппаратный reset. */
}; /* Пустые имена не создают ложных кнопок для резервных битов. */
uint32_t uksvep_can_id(unsigned device, unsigned from_device) /* Повторяет Setup_CAN_addr без связи с адресом boot Type=5. */
{ /* Весь DeviceID ограничен четырьмя перемычками платы. */
if (device > 15U || from_device > 1U) { /* Не обрезает ошибочный номер до адреса другого устройства. */
return UINT32_MAX; /* Явный недопустимый ID исключает скрытую передачу. */
} /* Направление 1 означает данные от эмулируемого или реального ВЭП. */
return 0x001CE000UL | device | (from_device ? 0U : 0x20U); /* Командное смещение 0x20 отличается от 0x10 в Бальзам. */
} /* Завершает единственное вычисление маршрутов UKSVEP. */
size_t uksvep_can_encode(unsigned device, unsigned from_device, unsigned address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity) /* Проверяет геометрию до общего кодека. */
{ /* Один кадр вмещает до трёх последовательных слов uint16. */
if (capacity < 8U || uksvep_can_id(device, from_device) == UINT32_MAX || address >= UKSVEP_REGISTER_COUNT || count == 0U || count > 3U || count > UKSVEP_REGISTER_COUNT - address) { /* Исключает переполнение банка R0…R127. */
return 0U; /* Не выдаёт частично собранный пакет ошибочного диапазона. */
} /* Параметры относятся к существующей карте платы. */
return balsam_can_encode_payload((uint16_t)address, values, count, output, capacity); /* Общий кодек задаёт порядок word2, word3, адрес/маска, word1. */
} /* Не содержит второго endian/масочного алгоритма. */
int uksvep_can_decode(unsigned device, uint32_t id, const uint8_t *data, size_t size, uint16_t *output, size_t capacity) /* Разбирает только выбранное устройство и обе его роли. */
{ /* ABI возвращает плоские скаляры без зависимости от выравнивания C-структур. */
balsam_can_frame_t frame = {0}; /* Общий payload decoder заполняет маску, адрес и три слова. */
unsigned index; /* Перебирает только реально присутствующие биты маски. */
unsigned count = 0U; /* Считает адресные пары, которые разрешено применить к RX-снимку. */
if (capacity < 8U || device > 15U || output == NULL || id > 0x1FFFFFFFUL) { /* Отвергает неверные аргументы до сравнения маршрутов. */
return -1; /* Ошибка не должна частично изменить снимок клиента. */
} /* Проверка чужого ID выполняется до DLC и указателя данных. */
if (id != uksvep_can_id(device, 0U) && id != uksvep_can_id(device, 1U)) { /* Broadcast и чужой ВЭП не выдаются за адресный ответ. */
return 0; /* Позволяет общему CAN-монитору передать кадр другим потребителям. */
} /* Дальше кадр гарантированно адресует выбранный профиль. */
if (data == NULL || size != 8U || !balsam_can_decode_payload(data, size, &frame) || frame.present_mask == 0U) { /* Требует полный classic CAN data payload с полезными словами. */
return -1; /* Повреждённый собственный кадр учитывается как ошибка RX. */
} /* Проверяет все адреса до публикации результата. */
for (index = 0U; index < 3U; ++index) { /* В кадре максимум три последовательных адреса. */
if ((frame.present_mask & (4U >> index)) != 0U && frame.start_address + index >= UKSVEP_REGISTER_COUNT) { /* Отсутствующее слово не создаёт ложной записи R128/R129. */
return -1; /* При выходе присутствующего слова за карту отвергается весь пакет. */
} /* Проверка проходит до заполнения плоского output. */
} /* Геометрия всех значащих слов проверена. */
output[0] = id == uksvep_can_id(device, 1U); /* Сохраняет реальное направление, чтобы собственный TX не стал ответом платы. */
for (index = 0U; index < 3U; ++index) { /* Сохраняет только слова с установленными битами присутствия. */
if ((frame.present_mask & (4U >> index)) != 0U) { /* Неприсутствующие нули не должны затирать предыдущие RX-значения. */
output[2U + count * 2U] = (uint16_t)(frame.start_address + index); /* Возвращает точный индекс регистра. */
output[3U + count * 2U] = frame.values[index]; /* Возвращает полезное uint16 без потери старшего бита. */
++count; /* Продвигает плоскую таблицу только после полезной пары. */
} /* Завершает применение одного бита маски. */
} /* Все полезные слова готовы к атомарному обновлению модели. */
output[1] = (uint16_t)count; /* Число пар делает ABI независимым от завершающих неиспользуемых элементов. */
return (int)count; /* Положительный результат отличает собственный валидный кадр от чужого. */
} /* Завершает общий декодер профиля без GPIO и Qt. */
unsigned uksvep_field_count(void) /* Предоставляет размер карты из единственного источника. */
{ /* Клиент не хранит вторую таблицу адресов. */
return (unsigned)(sizeof(fields) / sizeof(fields[0])); /* Размер определяется фактическим C-массивом. */
} /* Завершает запрос количества полей. */
int uksvep_field_info(unsigned index, unsigned device, uint16_t *output, size_t capacity) /* Возвращает метаданные в явном массиве четырёх uint16. */
{ /* Неверный адрес устройства не должен давать правдоподобные параметры другой платы. */
if (capacity < 4U || index >= uksvep_field_count() || device > 15U || output == NULL) { /* Проверяет границы всех входных аргументов. */
return 0; /* При отказе выходной буфер не изменяется. */
} /* Индекс описывает существующее именованное поле. */
output[0] = fields[index].address; /* Индекс слова modbus без дополнительного смещения. */
output[1] = fields[index].maximum; /* Ограничение ввода остаётся общим для GUI и будущих портов. */
output[2] = fields[index].address == 126U ? (uint16_t)(device + 1U) : fields[index].initial; /* Mode зависит от четырёх перемычек выбранного адреса. */
if (fields[index].address == 23U) { /* R23 содержит активные логические перемычки J0…J3 в младшем полубайте. */
output[2] = (uint16_t)device; /* Начальный эмулируемый адрес соответствует выбранному DeviceID без TEST. */
} /* Профиль не создаёт противоречия между исходящим ID и состоянием перемычек. */
output[3] = fields[index].flags; /* Роль определяет доступность редактирования реальной платы. */
return 1; /* Все четыре скаляра успешно заполнены. */
} /* Завершает запрос метаданных без передачи C padding через ABI. */
const char *uksvep_field_name(unsigned index) /* Возвращает UTF-8 имя именованного регистра. */
{ /* Неверный индекс не должен приводить к чтению произвольного указателя. */
return index < uksvep_field_count() ? fields[index].name : ""; /* Пустая строка явно означает отсутствие поля. */
} /* Память строки остаётся во владении библиотеки. */
const char *uksvep_input_name(unsigned bit) /* Возвращает назначение физического дискретного входа. */
{ /* В прошивке используются ровно четырнадцать разрядов R0. */
return bit < 14U ? inputs[bit] : ""; /* Резервные 14/15 не получают выдуманных GPIO. */
} /* Завершает запрос общей карты входов. */
const char *uksvep_command_name(unsigned bit) /* Возвращает назначение управляющего бита R127. */
{ /* Сохраняет пустые позиции резервных команд package.h. */
return bit < 16U ? commands[bit] : ""; /* Проверяет индекс до доступа к таблице. */
} /* Завершает запрос именованных команд. */
void uksvep_evaluate_inputs(uint16_t value, uint16_t alarm_mask, uint16_t error_mask, uint16_t *output, size_t capacity) /* Модель ReadEnteres для исходящих данных эмулируемого ВЭП. */
{ /* Расчёт используется только по явной кнопке и не считается ответом реальной платы. */
if (output == NULL || capacity < 2U) { /* Позволяет безопасно отвергнуть неверный буфер клиента. */
return; /* Не обращается к отсутствующей памяти. */
} /* Все действия ниже повторяют особые случаи битов 7/8/9 из lampa.c. */
output[0] = (uint16_t)((uint16_t)~value & alarm_mask & (uint16_t)~0x0380U); /* Обычные неисправности — отсутствие включённых в маску входов; 7/8/9 обрабатываются отдельно. */
output[0] |= (uint16_t)(value & 0x0300U); /* Разряд и авария 24 В всегда являются неисправностями. */
output[1] = (uint16_t)((uint16_t)~value & error_mask & (uint16_t)~0x0380U); /* Обычные аварии также определяются инверсией входов и маской. */
output[1] |= (uint16_t)((value & error_mask & 0x0100U) | (value & 0x0200U)); /* Разряд зависит от Error_mask.8, авария 24 В всегда активна при R0.9=1. */
} /* Заряд накопителя R0.7 никогда не создаёт неисправность либо аварию. */
+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. */
+58
View File
@@ -0,0 +1,58 @@
#include "uksvep_can.h" /* Проверяет C99-профиль, который вызывают все GUI через ABI. */
#include "balsam_can.h" /* Сохраняет регрессию существующей маршрутизации Бальзам после выделения общего payload. */
#include <assert.h> /* Неверный проводной вектор должен завершать тест с отказом. */
#include <string.h> /* Сравнивает байты с независимым эталоном, а не только с обратным decoder. */
int main(void) /* Фиксирует реальную карту UKSVEP из can.c/lampa.c/package.h. */
{ /* Проверки не требуют Qt, аппаратного CAN или микроконтроллера. */
const uint16_t words[] = {0x1234U, 0xABCDU, 0x8001U}; /* Различные слова выявляют перестановку и потерю старшего бита. */
const uint8_t golden[] = {0xABU, 0xCDU, 0x80U, 0x01U, 0xE0U, 0x08U, 0x12U, 0x34U}; /* R8…R10: word2, word3, маска111/адрес8, word1. */
const uint8_t sparse[] = {0xAAU, 0x55U, 0x33U, 0x44U, 0xA0U, 0x7DU, 0x11U, 0x22U}; /* Маска101 разрешает R125 и R127, оставляя R126 неизменным. */
uint8_t payload[8]; /* Выходной буфер реального кодировщика C. */
uint16_t decoded[8]; /* Плоский ABI буфер направления и пар регистров. */
uint16_t info[4]; /* Адрес, максимум, начальное значение и роль регистра. */
uint16_t state[2]; /* Расчётные неисправности и аварии эмулируемого ВЭП. */
balsam_can_frame_t previous = {0}; /* Независимо проверяет существующий публичный decoder Бальзам. */
unsigned device; /* Перебирает все положения четырёх перемычек. */
unsigned index; /* Перебирает общую карту подтверждённых полей. */
for (device = 0U; device < 16U; ++device) { /* Адреса не должны наследовать Mode=9 и базу BA0000 другого ВЭП. */
assert(uksvep_can_id(device, 0U) == 0x001CE020U + device); /* Командный ID берётся из Setup_CAN_addr текущего STM32-проекта. */
assert(uksvep_can_id(device, 1U) == 0x001CE000U + device); /* Исходящий ID ВЭП не содержит командного смещения 0x20. */
} /* Проверяет обе роли каждого физического адреса. */
assert(uksvep_can_id(16U, 0U) == UINT32_MAX && uksvep_can_id(0U, 2U) == UINT32_MAX); /* Ошибочная цель не обрезается до соседнего устройства. */
assert(uksvep_can_encode(5U, 0U, 8U, words, 3U, payload, 7U) == 0U); /* Малый выходной буфер не допускает записи восьми байтов за его границы. */
assert(uksvep_can_encode(5U, 0U, 8U, words, 3U, payload, sizeof(payload)) == 8U); /* Строит стандартный трёхсловный командный кадр. */
assert(memcmp(payload, golden, sizeof(golden)) == 0); /* Эталон проверяет endian, адрес и маску независимо от decoder. */
assert(uksvep_can_decode(5U, 0x001CE025U, golden, 8U, decoded, 8U) == 3); /* Распознаёт собственное командное направление. */
assert(decoded[0] == 0U && decoded[2] == 8U && decoded[3] == 0x1234U && decoded[6] == 10U && decoded[7] == 0x8001U); /* Возвращает исходные пары без потери разрядов. */
assert(uksvep_can_decode(5U, 0x001CE005U, sparse, 8U, decoded, 8U) == 2); /* Частичный RX не требует всех трёх последовательных регистров. */
assert(decoded[0] == 1U && decoded[2] == 125U && decoded[3] == 0x1122U && decoded[4] == 127U && decoded[5] == 0x3344U); /* Отсутствующий R126 не появляется среди обновлений. */
assert(uksvep_can_decode(5U, 0x001CE004U, golden, 8U, decoded, 8U) == 0); /* Данные другого адреса игнорируются. */
assert(uksvep_can_decode(5U, 0x00BA0018U, golden, 8U, decoded, 8U) == 0); /* Старый ВЭП проекта Бальзам не подменяет STM32 УКСВЭП. */
assert(uksvep_can_decode(5U, 0x001CE005U, golden, 7U, decoded, 8U) == -1); /* Повреждённый DLC собственного узла отвергается. */
assert(uksvep_can_encode(5U, 0U, 127U, words, 2U, payload, sizeof(payload)) == 0U); /* Запись второго слова не может пересечь R127. */
assert(uksvep_can_encode(5U, 0U, 127U, words, 1U, payload, sizeof(payload)) == 8U && payload[4] == 0x80U && payload[5] == 0x7FU); /* Одно последнее слово использует маску100 и нулевые соседи. */
assert(uksvep_can_decode(5U, 0x001CE005U, payload, 8U, decoded, 8U) == 1); /* Последний валидный регистр можно принять отдельно. */
payload[4] = 0xE0U; /* Искусственно делает присутствующими запрещённые R128/R129. */
assert(uksvep_can_decode(5U, 0x001CE005U, payload, 8U, decoded, 8U) == -1); /* Такой пакет отвергается целиком, без частичного применения R127. */
assert(balsam_can_decode(0x00BA0017U, golden, 8U, &previous) == 1 && previous.device == 8U && previous.values[1] == 0xABCDU); /* Общий payload сохраняет прежний маршрут и слова задатчика. */
for (index = 0U; index < uksvep_field_count(); ++index) { /* Каждое именованное поле должно принадлежать реальной карте. */
assert(uksvep_field_info(index, 7U, info, 4U) && info[0] < 128U && info[2] <= info[1]); /* Начальное значение не превышает UI-границу поля. */
assert(strlen(uksvep_field_name(index)) != 0U); /* Нет безымянных полей, которые выглядели бы пропущенной картой. */
if (info[0] == 126U) { /* Физический адрес отличается от Mode на единицу. */
assert(info[2] == 8U); /* DeviceID7 соответствует Mode8 независимо от названия устройства. */
} /* Остальные defaults одинаковы для всех адресов. */
} /* Размер карты задаёт C, а не отдельный список Python. */
assert(!uksvep_field_info(999U, 0U, info, 4U) && strlen(uksvep_input_name(14U)) == 0U); /* За границами нет фиктивных регистров и GPIO. */
assert(strstr(uksvep_input_name(9U), "PB12") != NULL && strlen(uksvep_command_name(4U)) == 0U); /* Особый прямой вход и резервная команда сохраняют реальное назначение. */
uksvep_evaluate_inputs(0x247FU, 0x277FU, 0x0200U, state, 2U); /* Моделирует исправные штатные входы и маски. */
assert(state[0] == 0U && state[1] == 0U); /* Исправное состояние не должно вызывать ложную аварию. */
uksvep_evaluate_inputs(0x247EU, 0x277FU, 0x0200U, state, 2U); /* Убирает только исправность ИП1. */
assert(state[0] == 1U && state[1] == 0U); /* Маска по умолчанию делает потерю ИП1 неисправностью, а не аварией. */
uksvep_evaluate_inputs(0x27FFU, 0U, 0U, state, 2U); /* Включает заряд, разряд и аварию 24 В при нулевых масках. */
assert(state[0] == 0x0300U && state[1] == 0x0200U); /* Заряд не влияет, разряд всегда неисправность, 24 В всегда авария. */
uksvep_evaluate_inputs(0x25FFU, 0U, 0x0100U, state, 2U); /* Разрешает аварийный разряд отдельным битом маски. */
assert(state[1] == 0x0100U); /* Особый бит 8 проверяется по своей полярности без инверсии. */
return 0; /* Все эталоны, границы и регрессия старого профиля выполнены. */
} /* Завершает независимую host-проверку C-профиля. */
+2
View File
@@ -24,6 +24,7 @@ INCLUDE = ROOT / "include"
JNI_INCLUDES: list[Path] = []
SOURCES = [
ROOT / "src" / name for name in (
"c2000_sci.c", # Прямая MSVC/CC-сборка включает общий автомат SCI F28335.
"set_modbus_gateway.c", # Экспортирует общий USB RTU/TCP кодек также при прямой MSVC/CC сборке без CMake.
"deif_xdi.c", # Общий DEIF входит в host-библиотеку.
"set_invertor_gas.c", # Собирает общий адаптер двух GAS для STM Invertor при прямом запуске host-builder.
@@ -33,6 +34,7 @@ SOURCES = [
"altera_stream.c",
"set_gas_watch.c", # Добавляет общий GAS_WATCH2 в прямые MSVC/CC сборки, чтобы новые ctypes/JNI символы были доступны.
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c", "set_signal.c", "set_wavegen.c",
"uksvep_can.c", # Включает общий профиль ВЭП также в прямую MSVC/CC сборку DLL.
"balsam_can.c", "set_crc.c", "periph28335.c", "tms2812.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
"pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",
"pcan_gas.c", "pcan_modbus_server.c",
-39
View File
@@ -1,39 +0,0 @@
# Общее ядро Modbus-шлюза
`c/set-protocol/include/set_modbus_gateway.h` и
`c/set-protocol/src/set_modbus_gateway.c` обслуживают RTU ↔ TCP,
проверку ответов и атомарную таблицу маршрутов. Код C99 не использует
heap, файловую систему, HAL, системные часы и глобальное состояние.
CRC16 берётся из существующего `set_crc.c`.
Слои: GUI / ARM-служба → общее C-ядро → QtSerialPort / Linux UART и TCP.
`python/set_devices/modbus_gateway.py` предоставляет ctypes-модели,
`python/set_devices/qt_ports/modbus_gateway_port.py` — очередь USB
и локальный TCP endpoint для переиспользования готового Modbus UI.
Физические tty, GPIO DE, Linux интерфейсы и карта FPGA остаются в проекте платы.
| API | Контракт |
|---|---|
| `smg_tcp_size`, `smg_rtu_size`, `smg_rtu_response_size` | Полный размер ADU, 0 для неполного префикса, -1 для неверного формата |
| `smg_decode_tcp`, `smg_decode_rtu` | Один целый ADU; RTU проверяется CRC до извлечения PDU |
| `smg_check` | FC01/02/03/04/05/06/15/16, количества, byte count, адресный диапазон |
| `smg_tcp`, `smg_rtu` | Только сериализация; явные размеры и владение памятью вызывающего |
| `smg_accept_tcp`, `smg_accept_rtu` | Корреляция, функция, ожидаемая длина и echo записи до выдачи PDU |
| `smg_config_pack`, `smg_config_unpack` | Частная FC65, версия 1, до 20 уникальных входных адресов |
Структуры `smg_request`, `smg_route`, `smg_config` являются native ABI,
а не wire-форматом. Нельзя передавать их через USB посредством `memcpy`.
Numeric IPv4 хранит значение адреса старшим октетом в старших битах.
Поля конфигурации сериализуются явно без padding C-компилятора.
Unit ID 248 зарезервирован для управления. FC65 op=1 читает настройки,
op=2 применяет целиком, op=3 возвращает подтверждённую модель. Заголовок
конфигурации имеет 13 байт вместе с FC/op/version; маршрут — 10 байт.
Поля маршрута: source, target, transport, interface, IPv4 BE32, port BE16.
Сериализация времени и baud выполняется C API; UI не знает этих смещений.
В качестве потребителя используется Zynq RK7020: `demo/modbus_gateway/server.c`,
панель SETGUI `pc/zynq_modbus_panel.py`. Аппаратные ограничения и установка
описаны в `zynq/docs/MODBUS_GATEWAY.md`. Публичный модуль собирается штатным
CMake set-protocol; `test_modbus_gateway` проверяет независимые эталонные байты,
CRC, границы, remap, Transaction ID и транзакционность конфигурации.
+55
View File
@@ -0,0 +1,55 @@
# Прошивка STM32 через J-Link
В SETGUI откройте **Сервис → Прошивка** и выберите **STM32 / J-Link JTAG или SWD**.
Установленный SEGGER J-Link Commander обнаруживается в Keil, каталоге SEGGER или PATH.
При необходимости укажите `JLink.exe` кнопкой «Обзор». Одноимённый инструмент Java JDK программатором не является.
Выберите микроконтроллер из базы J-Link, JTAG либо SWD, частоту в кГц и фактический объём Flash в КиБ.
Начальная частота — 1000 кГц. При нескольких подключённых J-Link укажите серийный номер нужного программатора.
COM-порт и доступ к BOOT0 для этого режима не требуются.
Выберите HEX или BIN и нажмите «Начать прошивку». HEX сохраняет собственные абсолютные адреса и разрывы;
для BIN укажите адрес в поле «Адрес приложения». При изменении файла после выбора требуется выбрать его заново.
Если есть manifest выпуска, его target должен соответствовать выбранному MCU.
Каждая операция создаёт отдельный каталог в выбранной папке резервных копий:
- `before.bin` — вся Flash до записи;
- `image.hex` — проверенный разреженный снимок записываемых данных;
- `after.bin` — вся Flash после записи;
- `program.jlink`, `program.log`, `program-dll.log` — команды и результаты программирования;
- `run.jlink`, `run.log`, `run-dll.log` — отдельный этап сброса и запуска.
Программирование выполняется через `loadfile` без команды полного стирания.
После встроенной проверки J-Link выполняется независимое побайтное сравнение readback с планом.
Только после успешного сравнения отдельный процесс делает reset и `g`.
Нулевой код выхода без полных снимков Flash не считается успешной записью.
Отмена останавливает собственный процесс Commander; при прерванной записи нужна повторная прошивка.
## UKSVEP STM32F103RC
Для текущей платы оставьте включённым профиль UKSVEP, устройство `STM32F103RC`, объём 256 КиБ.
Приложение начинается с `0x08008000`. Страница параметров `0x0801F800..0x0801FFFF` запрещена для записи,
её сохранность проверяется сравнением `before.bin` и `after.bin`.
Для первой установки выберите разреженный `uksvep_install.hex`, содержащий boot и приложение.
Для обновления через JTAG можно выбрать обычный HEX приложения или BIN с адресом `0x08008000`.
Общий план добавит manifest CRC32 по адресу `0x0803F800`, который проверяет постоянный загрузчик.
Готовый manifest установочного HEX сверяется с приложением. Перед запуском снимаются старые одноразовые
маркеры boot в BKP DR9/DR10 без стирания резервного домена.
Для других плат снимите флажок UKSVEP и задайте их микроконтроллер и объём Flash.
Защита страницы и формирование manifest UKSVEP относятся только к этому явно выбранному профилю.
## Расположение общей реализации
- `python/set_devices/stm32_jtag.py` — неизменяемые настройки, проверка образа, разреженный HEX, сценарии и readback;
- `python/set_devices/qt_ports/stm32_jtag.py` — асинхронный порт QProcess, журналы, тайм-аут, отмена и reset/run;
- `python/set_devices/qt_ports/stm32_jtag_panel.py` — встраиваемая форма Qt 5/6 с сохранением настроек;
- `python/tests/test_stm32_jtag.py` — проверки плана, защиты параметров и ошибок без физического MCU.
Модули не импортируют `gui_desktop`. SETGUI подключает панель к существующим кнопкам, прогрессу и терминалу.
Код загрузчика `stm32-jtag`, транспорт `stm32`, соответствует индексу 8 формы;
профиль `flashing.profiles.stm32-jtag` может задавать `base_address`.
Параметры конкретного J-Link сохраняются локально и не публикуются вместе с прошивкой.
SEGGER Commander должен быть установлен отдельно; пакет SETGUI не скачивает и не включает сторонний executable.
+3 -3
View File
@@ -96,11 +96,11 @@ module la_trigger_buffer #(
previous_valid <= 1;
if (history != 0)
history <= history - 1'b1;
write_address <= write_address + 1'b1; // Внутренний кольцевой индекс продвигается на каждом отсчёте, включая ожидание без pretrigger: неписавшиеся позиции не наблюдаемы, а fire фиксирует соответствующее начало чтения; это убирает поздний trigger из CE 16-битного указателя.
if (!triggered) // До срабатывания отслеживаем только потенциальное начало; после fire старое значение triggered=0 позволяет зафиксировать точный адрес на том же фронте без нового такта задержки.
read_address <= write_address - pre; // Индексы заданы в отсчётах по модулю 2^ADDR_WIDTH: prospective адрес игнорируется до готового окна, затем удерживается на первом pretrigger-отсчёте.
if (pre != 0 || triggered || fire)
write_address <= write_address + 1'b1;
if (fire) begin
triggered <= 1;
read_address <= write_address - pre;
if (post_left == 0) begin
capturing <= 0;
read_pending <= 3;
+146
View File
@@ -0,0 +1,146 @@
"""Расчёт токов асинхронного двигателя по состояниям ключей инвертора.""" # Модель не зависит от GUI, sigrok и сторонних пакетов Python.
import cmath # Комплексные числа представляют неподвижные электрические оси α и β.
import math # Проверки параметров и преобразования СИ используют стандартную библиотеку.
def finite(value, name, minimum=None, strict=False): # Единая проверка исключает NaN, бесконечности и нефизические параметры.
number = float(value) # Параметры sigrok и непосредственных вызовов приводятся к одному числовому типу.
if not math.isfinite(number) or minimum is not None and (number <= minimum if strict else number < minimum): # Строгая граница применяется к сопротивлениям и индуктивностям.
raise ValueError('{} has an invalid value: {}'.format(name, value)) # Сообщение указывает конкретную настройку пользователя.
return number # Возвращается только проверенное значение в единицах вызывающей стороны.
def clarke(values): # Амплитудно-инвариантное преобразование сохраняет амплитуду симметричных фаз.
return complex((2 * values[0] - values[1] - values[2]) / 3, (values[1] - values[2]) / math.sqrt(3)) # Оси αβ соответствуют порядку фаз A, B, C.
def phases(vector): # Обратное преобразование восстанавливает три фазы трёхпроводной нагрузки.
return (vector.real, -vector.real / 2 + math.sqrt(3) * vector.imag / 2, -vector.real / 2 - math.sqrt(3) * vector.imag / 2) # Сумма фаз равна нулю по первому закону Кирхгофа.
class InductionMotor: # Линейная модель короткозамкнутого двигателя использует приведённые к статору параметры ротора.
def __init__(self, rs=1.0, rr=0.8, lls_mh=5.0, llr_mh=5.0, lm_mh=150.0, pole_pairs=2, speed_rpm=1440.0, initial_flux=0.0, flux_angle=0.0, ia=0.0, ib=0.0): # Значения по умолчанию являются примером, а не паспортом конкретного двигателя.
self.rs = finite(rs, 'Stator R (ohm)', 0, True) # Положительное сопротивление одной обмотки статора задаётся в омах.
self.rr = finite(rr, 'Rotor R referred to stator (ohm)', 0, True) # Сопротивление ротора должно быть приведено к той же обмотке статора.
lls = finite(lls_mh, 'Stator leakage L (mH)', 0, True) / 1000 # Миллигенри переводятся в генри перед вычислением уравнений.
llr = finite(llr_mh, 'Rotor leakage L (mH)', 0, True) / 1000 # Параметр ротора также приводится к статору и переводится в генри.
lm = finite(lm_mh, 'Magnetizing L (mH)', 0, True) / 1000 # Взаимная индуктивность задаёт магнитную связь без насыщения.
pairs = finite(pole_pairs, 'Pole pairs', 1) # Число пар полюсов переводит механическую скорость в электрическую.
if pairs != int(pairs): # Дробное число пар полюсов не описывает физический двигатель.
raise ValueError('Pole pairs must be an integer.') # Ошибка выдаётся до расчёта токов.
omega = finite(speed_rpm, 'Mechanical speed (rpm)') * pairs * math.tau / 60 # Один оборот равен 2π радиан, одна минута содержит 60 секунд.
lr = llr + lm # Полная индуктивность ротора включает рассеяние и намагничивание.
self.lsigma = lls + lm * llr / lr # Устойчивая форма Ls−Lm²/Lr не теряет точность при малом рассеянии.
self.k = lm / lr # Коэффициент связи переводит потокосцепление ротора в уравнение статора.
self.a = -(self.rs + self.rr * self.k * self.k) / self.lsigma # Первый диагональный коэффициент учитывает потери статора и приведённого ротора.
self.b = self.k * complex(self.rr / lr, -omega) / self.lsigma # Влияние потока ротора включает его вращение относительно неподвижных осей.
self.c = self.rr * self.k # Ток статора возбуждает поток ротора через взаимную индуктивность.
self.d = complex(-self.rr / lr, omega) # Поток ротора затухает с постоянной Lr/Rr и вращается с электрической скоростью ротора.
self.det = self.a * self.d - self.b * self.c # Определитель нужен для точного вынужденного решения при постоянном напряжении.
self.mid = (self.a + self.d) / 2 # Половина следа матрицы задаёт среднее собственных значений.
self.root = cmath.sqrt(((self.a - self.d) / 2) ** 2 + self.b * self.c) # Разность собственных значений позволяет вычислить матричную экспоненту без numpy.
initial = (finite(ia, 'Initial Ia (A)'), finite(ib, 'Initial Ib (A)')) # Начальные линейные токи задаются в амперах.
self.current = clarke((initial[0], initial[1], -sum(initial))) # Третья фаза определяется условием трёхпроводного подключения.
self.flux = finite(initial_flux, 'Initial rotor flux linkage (Wb)', 0) * cmath.exp(1j * math.radians(finite(flux_angle, 'Initial rotor flux angle (deg)'))) # Модуль и угол задают неизвестное при начале записи начальное потокосцепление.
self.cache = {} # Повторяющиеся интервалы переключений используют готовую матрицу перехода.
def advance(self, seconds, voltage): # Между фронтами ключей напряжение неизменно, поэтому используется точное линейное решение.
dt = finite(seconds, 'Time interval (s)', 0) # Отрицательное время и неконечные интервалы запрещены.
if not dt: # Повторные события одного отсчёта меняют ключи без изменения физического состояния.
return phases(self.current) # Начальный ток сохраняется без искусственного шага интегрирования.
matrix = self.cache.get(dt) # На равномерной сетке большинство длительностей повторяется.
if matrix is None: # Новая длительность требует вычисления exp(A·dt) ровно один раз.
if abs(self.root * dt) < 1e-8: # При близких собственных значениях деление на их разность теряет точность.
factor = cmath.exp(self.mid * dt) # Общая экспонента остаётся ограниченной для пассивного двигателя.
ratio = factor * dt * (1 + (self.root * dt) ** 2 / 6) # Разложение sinh(z)/z сохраняет точность около нуля.
center = factor * (1 + (self.root * dt) ** 2 / 2) # Разложение cosh(z) согласовано с тем же пределом.
else: # Раздельные экспоненты избегают переполнения cosh при сильно различающихся постоянных времени.
first, second = cmath.exp((self.mid + self.root) * dt), cmath.exp((self.mid - self.root) * dt) # Обе моды учитывают затухание и вращение потока ротора.
ratio, center = (first - second) / (2 * self.root), (first + second) / 2 # Эти коэффициенты задают экспоненту матрицы размерности два.
matrix = (center + ratio * (self.a - self.mid), ratio * self.b, ratio * self.c, center + ratio * (self.d - self.mid)) # Порядок элементов соответствует строкам тока статора и потока ротора.
if len(self.cache) >= 1024: # Ограничение в 1024 различных длительности исключает рост памяти на нерегулярном ШИМ.
self.cache.clear() # Сброс кэша влияет только на производительность, а не на состояние двигателя.
self.cache[dt] = matrix # Сохраняется только матрица перехода, независимая от текущего напряжения.
drive = clarke(voltage) / self.lsigma # Фазные напряжения переводятся в неподвижный вектор скорости изменения тока.
equilibrium_i, equilibrium_flux = -self.d * drive / self.det, self.c * drive / self.det # Частное решение −A⁻¹B·u учитывает постоянное напряжение на всём интервале.
delta_i, delta_flux = self.current - equilibrium_i, self.flux - equilibrium_flux # Начальное состояние переносится относительно частного решения.
self.current = equilibrium_i + matrix[0] * delta_i + matrix[1] * delta_flux # Ток обновляется без явного Эйлера и зависимости устойчивости от частоты отображения.
self.flux = equilibrium_flux + matrix[2] * delta_i + matrix[3] * delta_flux # Поток использует прежний ток, сохранённый до обновления состояния.
if not all(math.isfinite(value) for value in (self.current.real, self.current.imag, self.flux.real, self.flux.imag)): # Чрезмерные параметры не должны порождать молчаливые NaN в графиках.
raise ValueError('Motor state overflow: check motor parameters and capture timing.') # Сообщение связывает ошибку с физическими параметрами и временем записи.
return phases(self.current) # Возвращаются мгновенные токи трёх эквивалентных обмоток в амперах.
class PwmMotor: # Связь логических входов, силовой схемы и общего двигателя не содержит зависимости от DSView.
def __init__(self, samplerate, topology='2-level', dc_voltage=560.0, midpoint_percent=50.0, connection='star', active_high=True, **motor): # Полное напряжение звена задаётся между положительной и отрицательной шинами.
self.rate = finite(samplerate, 'Samplerate (Hz)', 0, True) # Частота захвата переводит номера отсчётов в секунды.
if topology not in ('2-level', '3-level P/N', '3-level NPC') or connection not in ('star', 'delta'): # Поддерживаются явные схемы без автоматического угадывания кодировки.
raise ValueError('Unsupported inverter topology or winding connection.') # Неизвестная схема не подменяется другой электрической моделью.
dc = finite(dc_voltage, 'Total DC link (V)', 0, True) # Значение в вольтах относится ко всему звену, а не к его половине.
split = finite(midpoint_percent, 'Upper DC link (%)', 0, True) / 100 # Доля верхней половины позволяет моделировать дисбаланс конденсаторов.
if split >= 1: # Обе половины звена должны иметь строго положительное напряжение.
raise ValueError('Upper DC link fraction must be between 0 and 100 percent.') # Граничные значения не описывают трёхуровневое звено.
self.upper, self.lower = dc * split, -dc * (1 - split) # Потенциалы полюсов отсчитываются от средней точки конденсаторов.
self.topology, self.connection, self.active_high = topology, connection, bool(active_high) # Параметры сохраняются для интерпретации каждой новой комбинации ключей.
ia, ib = motor.get('ia', 0.0), motor.get('ib', 0.0) # Начальные токи пользователь задаёт в линейных проводах независимо от схемы обмоток.
if connection == 'delta': # В симметричном треугольнике линейный ток равен разности токов соседних обмоток.
motor.update(ia=(2 * ia + ib) / 3, ib=(ib - ia) / 3) # Обратное преобразование предполагает отсутствие циркулирующего тока нулевой последовательности.
self.motor = InductionMotor(**motor) # Параметры Rr и Lr всегда приведены к одной фактической обмотке статора.
self.sample, self.pins, self.poles = None, None, (0.0, 0.0, 0.0) # До первого отсчёта не создаётся вымышленный участок записи.
self.warnings = () # Диагностика сообщает о комбинациях, не задающих однозначное выходное напряжение.
self.interval_warnings = () # Завершённый интервал сохраняет собственную диагностику до приёма следующего состояния затворов.
def currents(self): # Публичные результаты относятся к проводам A, B, C, подключённым к инвертору.
a, b, c = phases(self.motor.current) # Эквивалентные токи обмоток сохраняют условие отсутствия нулевой последовательности.
return (a - c, b - a, c - b) if self.connection == 'delta' else (a, b, c) # Для треугольника выводятся линейные токи, а не токи внутри обмоток.
def set_gates(self, pins): # Нумерация: Q1/P фаз A/B/C, Q4/N фаз A/B/C, затем Q2/Q3 каждой фазы.
if len(pins) != 12 or any(value not in (0, 1, None) for value in pins): # Отсутствующий логический канал обозначается None до нормализации полярности.
raise ValueError('Expected twelve gate inputs containing 0, 1 or None.') # Служебные значения sigrok не принимаются за реальный логический уровень.
bits = tuple(None if value is None else bool(value) == self.active_high for value in pins) # Инверсия применяется только к реально назначенным входам.
required = range(3) if self.topology == '2-level' else range(6) if self.topology == '3-level P/N' else range(12) # Три, шесть или двенадцать каналов нужны выбранной схеме.
if any(bits[index] is None for index in required): # Неполная трёхфазная схема не позволяет определить линейные напряжения двигателя.
raise ValueError('Assign all gate channels required by the selected topology.') # Ошибка возникает до продолжения расчёта по отсутствующим сигналам.
if self.topology == '2-level' and any(value is not None for value in bits[3:6]) and any(value is None for value in bits[3:6]): # Нижние ключи двухуровневой схемы назначаются одновременно для всех фаз.
raise ValueError('Assign either all three lower gates or none.') # Частично измеренные нижние ключи не смешиваются с идеальными дополнениями.
poles, warnings = [], [] # Каждая фаза сначала получает собственный потенциал и список неоднозначностей.
currents = self.currents() # Направление линейного тока задаёт приближённую проводимость обратных диодов во время dead time.
for phase in range(3): # Все входы одного отсчёта обрабатываются одновременно, без ложных промежуточных состояний.
high, low = bits[phase], bits[phase + 3] # В P/N это команды положительного и отрицательного уровней; в NPC это внешние Q1 и Q4.
state = (high, low) # Стандартная таблица P/N использует только два внешних логических входа.
table = {(True, False): self.upper, (False, True): self.lower, (False, False): 0.0} # Состояние 00 задаёт среднюю точку только в явной кодировке P/N.
if self.topology == '2-level': # Один вход на фазу означает идеальную комплементарную пару без измеренного dead time.
if low is None: # Нижняя команда восстанавливается только когда пользователь не назначил соответствующие входы.
low = not high # Низкий уровень верхней команды соответствует отрицательной шине.
state = (high, low) # Реальные нижние команды сохраняют интервалы одновременного закрытия ключей.
table = {(True, False): self.upper, (False, True): self.lower} # Одновременное включение двух ключей не имеет допустимого фазного напряжения.
if self.topology == '3-level NPC': # NPC обрабатывает четыре отдельных затворных сигнала каждого плеча.
state = (high, bits[6 + 2 * phase], bits[7 + 2 * phase], low) # Физический порядок ключей идёт сверху вниз от положительной шины к отрицательной.
table = {(True, True, False, False): self.upper, (False, True, True, False): 0.0, (False, False, True, True): self.lower} # Допустимые состояния Q1…Q4 равны 1100, 0110 и 0011.
if state in table: # Однозначная команда непосредственно определяет полюсное напряжение.
pole = table[state] # Токи интегрируются по реальному напряжению, а не по сглаженному заполнению ШИМ.
elif not any(state): # Полностью закрытое плечо проводит через обратные диоды при ненулевом токе.
pole = self.lower if currents[phase] > 1e-9 else self.upper if currents[phase] < -1e-9 else 0.0 # Порог 1 нА подавляет выбор шины по ошибке округления около нулевого тока.
warnings.append('Phase {}: diode/dead-time approximation'.format('ABC'[phase])) # Неизвестные падения на диодах и смена проводимости явно отмечаются как приближение.
else: # Недопустимые перекрытия и неполные состояния NPC не интерпретируются как достоверное напряжение.
pole = self.poles[phase] # Последний определённый потенциал используется только как явно помеченное продолжение оценки.
warnings.append('Phase {}: invalid gates {}; current estimate unreliable'.format('ABC'[phase], ''.join(str(int(value)) for value in state))) # Диагностика содержит фазу и фактическую комбинацию ключей.
poles.append(pole) # Все три полюса сохраняются до вычисления напряжений обмоток.
self.pins, self.poles, self.warnings = bits, tuple(poles), tuple(warnings) # Состояние относится к интервалу после текущего одновременного переключения.
def update(self, sample, pins=None): # Сначала завершается предыдущий электрический интервал и только затем меняются ключи.
stamp = int(sample) # Номера отсчётов сохраняются целыми, чтобы исключить накопление ошибки временной сетки.
if stamp != sample or stamp < 0 or self.sample is not None and stamp < self.sample: # Отсчёты должны поступать в хронологическом порядке.
raise ValueError('Sample numbers must be nonnegative, integral and monotonic.') # Ошибка входной последовательности не маскируется нулевой длительностью.
previous, start = self.currents(), self.sample # Левый конец сегмента берётся до интегрирования предыдущего напряжения.
self.interval_warnings = self.warnings # Выходной пакет должен описывать ключи рассчитанного интервала, а не уже принятую новую команду.
if start is not None and stamp > start: # Первый отсчёт определяет только начальное состояние записи.
a, b, c = self.poles # Полюсные напряжения неизменны с прошлого обработанного фронта.
voltage = (a - b, b - c, c - a) if self.connection == 'delta' else (a, b, c) # Преобразование Кларка само вычитает плавающий потенциал нейтрали звезды.
self.motor.advance((stamp - start) / self.rate, voltage) # Точное решение учитывает длительность каждого фактического состояния ключей.
self.sample = stamp # Обновление временной отметки предшествует приёму новых команд того же отсчёта.
if pins is not None: # Периодические точки графика могут приходить без нового фронта.
self.set_gates(pins) # Новые уровни действуют только справа от своей временной отметки.
elif self.pins is not None: # Переоценка диодов на сетке ограничивает ошибку около смены знака тока.
self.set_gates(tuple(None if value is None else int(value == self.active_high) for value in self.pins)) # Логические уровни переводятся обратно во входную полярность без двойной инверсии.
return None if start is None or stamp == start else (start, stamp, previous, self.currents()) # Сегмент содержит физические амперы на обоих концах интервала.
+152 -1
View File
@@ -19,6 +19,11 @@ class PwmChannel:
self.window_index = 1
self.high = 0
self.previous = None
self.carrier_start = self.carrier_period = None # Два действительных активных фронта нужны для измерения несущей; начальный уровень не считается фронтом.
self.carrier_count = self.carrier_samples = 0 # Счётчик полных периодов и их суммарная длина в отсчётах позволяют усреднять несущую без хранения записи.
self.field_previous = self.field_crossing = self.field_pending = None # Интерполированное пересечение уровня 50% отделено от подтверждённого полного периода поля.
self.field_armed = False # До отрицательной полуволны начальный фрагмент огибающей не может открыть измерение поля.
self.field_carrier = None # Снимок счётчиков несущей привязывает среднюю частоту к последнему завершённому периоду поля.
def next_boundary(self):
if self.window is None or self.origin is None:
@@ -34,9 +39,33 @@ class PwmChannel:
alpha = -math.expm1(-(midpoint - previous[0]) / self.tau)
value = previous[1] + alpha * (value - previous[1])
self.previous = (midpoint, value)
carrier = self.rate / self.carrier_period if self.carrier_period and end - self.carrier_start <= 2 * self.carrier_period else None # После двух периодов без фронтов прежняя несущая больше не считается текущим измерением.
field = self._field(midpoint, value) # Частота поля определяется по той же нормированной огибающей, которая выводится на график.
return dict(start=start, end=end, duty=duty, value=value,
frequency=self.rate / (end - start), midpoint=midpoint,
previous=previous)
previous=previous, carrier_frequency=carrier, field=field) # Частота окна остаётся совместимой со старым контрактом, измеренная несущая и полный период поля передаются отдельно.
def _field(self, sample, value): # Потоковый детектор измеряет электрическую частоту между одноимёнными пересечениями уровня заполнения 50%.
previous = self.field_previous # Предыдущая точка нужна для линейной интерполяции времени пересечения между серединами периодов ШИМ.
self.field_previous = (sample, value) # Храним только последнюю точку, поэтому память не зависит от длительности захвата.
if value <= -0.02: # Гистерезис 0,02 нормированной шкалы соответствует одному процентному пункту заполнения ниже 50%.
self.field_armed = True # Отрицательная полуволна разрешает одно следующее положительное пересечение.
if self.field_armed and previous is not None and previous[1] <= 0 < value: # Учитываем только восходящий переход, чтобы не удвоить электрическую частоту.
self.field_pending = previous[0] - previous[1] * (sample - previous[0]) / (value - previous[1]) # Интерполяция устраняет квантование периода поля шагом несущей.
if value <= 0: # Возврат через нуль до положительного порога означает неподтверждённое пересечение.
self.field_pending = None # Колебания внутри полосы гистерезиса не создают отдельные периоды.
if not self.field_armed or self.field_pending is None or value < 0.02: # Положительный порог также отстоит от 50% на один процентный пункт заполнения.
return None # Постоянное заполнение и неполные полуволны не получают выдуманную частоту поля.
crossing = self.field_pending # Подтверждение использует время нулевого пересечения, а не более позднее достижение порога.
last = self.field_crossing # Два подтверждённых восходящих пересечения ограничивают один полный электрический период.
counters = (self.carrier_count, self.carrier_samples) # Снимок относится только к действительно наблюдавшимся активным фронтам.
old_counters = self.field_carrier # Разность накопленных счётчиков даёт среднюю несущую за текущий период поля.
self.field_crossing, self.field_carrier = crossing, counters # Следующий период начинается с текущего подтверждённого пересечения.
self.field_armed, self.field_pending = False, None # Новое измерение разрешается только после следующей отрицательной полуволны.
if last is None: # Первое пересечение задаёт начало, но ещё не содержит полного периода.
return None # Начальный неполный фрагмент записи исключён из результата.
count, elapsed = counters[0] - old_counters[0], counters[1] - old_counters[1] # Длительность измеренных периодов несущей выражена в отсчётах, а не миллисекундах.
return dict(start=round(last), end=round(crossing), frequency=self.rate / (crossing - last), carrier_frequency=self.rate * count / elapsed if elapsed else None) # Полный период поля получает частоту в Гц и среднюю измеренную несущую, если за интервал были фронты.
def update(self, sample, pin):
level = bool(pin) == self.active_high
@@ -48,6 +77,8 @@ class PwmChannel:
if sample < self.last:
raise ValueError('Samples must be monotonic.')
events = []
if level != self.level and level and self.window is None: # В автоматическом режиме новый активный фронт одновременно завершает текущий период ШИМ.
self._carrier_edge(sample) # Обновляем измерение до выдачи завершённого периода, чтобы частота относилась именно к нему.
if self.window is not None:
boundary = self.next_boundary()
while boundary <= sample:
@@ -58,6 +89,8 @@ class PwmChannel:
self.window_index += 1
boundary = self.next_boundary()
self.high += (sample - self.last) * self.level
if level != self.level and level: # В режиме окон частота несущей всё равно измеряется по фронтам, независимо от заданного размера окна.
self._carrier_edge(sample) # Фронт на границе окна относится к следующему окну и не меняет уже выданное измерение.
elif level != self.level:
if level:
if self.start is not None and self.fall is not None:
@@ -67,3 +100,121 @@ class PwmChannel:
self.fall = sample
self.last, self.level = sample, level
return events
def _carrier_edge(self, sample): # Оба режима восстановления используют единый счётчик фактических периодов несущей.
if self.carrier_start is not None and sample > self.carrier_start: # Первый фронт только задаёт начало; нулевой период не допускается.
self.carrier_period = sample - self.carrier_start # Интервал активный фронт — активный фронт задаёт полный период ШИМ в отсчётах.
self.carrier_count += 1 # Счётчик нужен для среднего числа полных периодов за электрический цикл.
self.carrier_samples += self.carrier_period # Суммируем длительности, чтобы переменная несущая усреднялась как число периодов за время.
self.carrier_start = sample # Следующее измерение начинается с текущего реального фронта.
class NpcChannel(PwmChannel): # Один вход NPC объединяет четыре затвора S1…S4, расположенные сверху вниз в фазной стойке.
STATES = {(1, 1, 0, 0): 1, (0, 1, 1, 0): 0, (0, 0, 1, 1): -1} # Допустимые состояния классического NPC дают +Udc/2, нейтраль и −Udc/2 соответственно.
def __init__(self, samplerate, active_high=True, carrier_hz=0, smoothing_ms=0): # Настройки времени и полярности совпадают с двухуровневым режимом.
super().__init__(samplerate, active_high, carrier_hz, smoothing_ms) # Общий расчёт частоты поля и фильтра применяется к среднему знаковому уровню NPC.
self.integral = self.known = 0 # Интеграл уровня выражен в отсчётах; known хранит длительность достоверных состояний.
self.last_valid = self.pulse_sign = None # Пропуск dead-time не должен ошибочно создавать новый импульс той же полярности.
def state(self, gates): # Преобразование использует все четыре затвора, а не только два внешних ключа.
return self.STATES.get(tuple(int(bool(pin) == self.active_high) for pin in gates)) # Неизвестное состояние, включая все выключенные ключи, возвращает None вместо выдуманного нуля напряжения.
def _accumulate(self, sample): # Между цифровыми событиями уровень стойки считается постоянным только при допустимой комбинации затворов.
if self.level is not None: # Во время dead-time напряжение зависит от тока и диодов и не определяется одними управляющими сигналами.
self.integral += (sample - self.last) * self.level # Знаковая площадь сохраняет отличие положительных, нулевых и отрицательных участков.
self.known += sample - self.last # В среднем используются лишь участки, однозначно восстановленные по затворам.
self.last = sample # Новый интервал начинается после полностью учтённой части времени.
def _npc_emit(self, start, end): # Среднее по допустимым состояниям является оценкой огибающей, а не измерением напряжения в dead-time.
coverage = self.known / (end - start) # Доля известного времени позволяет отделить короткие коммутационные паузы от повреждённой записи.
if coverage < 0.95: # При более чем 5% неизвестного времени в периоде оценка считается недостаточно определённой и пропускается.
self.previous = self.field_previous = self.field_crossing = self.field_pending = self.field_carrier = None # Разрыв не должен соединяться графиком или измерением поля с предыдущим достоверным участком.
self.field_armed = False # Новое поле должно заново подтвердить отрицательную полуволну.
return [] # Неизвестный интервал не превращается в фиктивный уровень нуля.
high = (self.integral / self.known + 1) * (end - start) / 2 # Общий _emit ожидает эквивалентную активную длительность; преобразование сохраняет средний уровень −1…+1.
event = self._emit(start, end, high) # Общая обработка формирует середину периода, частоты и фильтрованное значение.
event['valid_fraction'] = coverage # Точность определения уровня видна в числовой аннотации и экспорте.
return [event] # Формат списка совместим с обновлением двухуровневого канала.
def update(self, sample, gates): # Одно событие получает атомарный снимок четырёх цифровых входов.
level = self.state(gates) # Только три штатные комбинации имеют определённый нормированный уровень стойки.
if self.last is None: # Начальный уровень не является обнаруженным фронтом несущей.
self.last, self.level, self.last_valid = sample, level, level # Состояние начинается с текущего снимка без выдуманного предыдущего импульса.
if self.window is not None: # В режиме заданной несущей интегрирование начинается с области декодирования.
self.origin = self.start = sample # Границы окон совпадают с прежним контрактом PwmChannel.
return [] # Начальная точка ещё не содержит полного периода.
if sample < self.last: # Равные номера допустимы для одновременных фронтов разных каналов.
raise ValueError('Samples must be monotonic.') # Отрицательные интервалы нельзя интегрировать или выдавать в DSView.
entry = level in (-1, 1) and level != self.last_valid # Начало ненулевого импульса распознаётся с учётом последнего достоверного состояния до dead-time.
events = [] # Результаты содержат лишь полные периоды или окна.
if self.window is not None: # Фиксированные окна сохраняют 0/100% и работают при отсутствии переключений.
boundary = self.next_boundary() # Граница рассчитывается от начала, поэтому дробный период не накапливает ошибку округления.
while boundary <= sample: # Одно цифровое событие может завершить несколько окон постоянного уровня.
self._accumulate(boundary) # Учёт предыдущего состояния заканчивается точно на границе окна.
events.extend(self._npc_emit(self.start, boundary)) # Неизвестные участки проверяются до формирования результата.
self.start = boundary # Следующее окно начинается без перекрытия с предыдущим.
self.integral = self.known = 0 # Площадь и известное время относятся только к новому окну.
self.window_index += 1 # Номер следующей границы задаёт её положение в общей шкале отсчётов.
boundary = self.next_boundary() # Повторное вычисление сохраняет точность дробной частоты.
self._accumulate(sample) # Оставшаяся часть предыдущего уровня учитывается до изменения затворов.
if entry: # Частота несущей измеряется по началам последовательных ненулевых импульсов одинакового знака.
if self.pulse_sign == level: # Переход положительной полуволны в отрицательную не является коротким периодом несущей.
self._carrier_edge(sample) # Измеренный период не подменяется частотой заданного окна.
if self.window is None and self.start is not None: # Автоматический режим завершает период на одноимённом фронте следующего импульса.
events.extend(self._npc_emit(self.start, sample)) # Огибающая получается из интеграла всех трёх уровней за полный период.
else: # Первое появление другой полярности заново синхронизирует несущую этой полуволны.
self.carrier_start, self.carrier_period = sample, None # Интервал через смену знака не принимается за измеренную частоту.
self.pulse_sign = level # Следующий ненулевой импульс сравнивается с текущей полярностью.
if self.window is None: # В автоматическом режиме начало и интеграл обновляются только на опорных фронтах.
self.start = sample # Начальный неполный фрагмент полуволны исключается.
self.integral = self.known = 0 # Новый период не наследует площадь предыдущего.
self.level = level # Новое состояние действует только после текущего цифрового события.
if level is not None: # Неизвестная коммутационная пауза не стирает информацию о последнем допустимом уровне.
self.last_valid = level # Это исключает ложные импульсы при кратком выключении и возврате к прежней комбинации.
return events # Адаптер получает тот же поток результатов, что для обычного ШИМ.
class PhaseAnalysis: # Согласование времён A/B/C отделено от конкретного числа затворов в фазе.
def __init__(self, samplerate): # Частота нужна для контроля устаревших оценок фазового периода.
self.rate = samplerate # Все временные координаты остаются в отсчётах захвата.
self.points, self.fields = {}, {} # Для интерполяции хранятся две последние точки каждой огибающей и последний полный период поля.
self.last_vector = None # Одинаковый общий момент времени не должен выдавать повторный угол.
def add(self, channel, event): # Каждый восстановленный период обновляет только свою фазу.
points = self.points.setdefault(channel, []) # Отсутствующая фаза не получает нулевой или выдуманный сигнал.
if event.get('previous', True) is None: # Ядро явно разрывает огибающую после недостаточно определённого окна NPC.
points.clear() # Интерполяция не должна соединять новую точку с данными до неизвестного участка.
self.fields.pop(channel, None) # Межфазные углы также должны заново дождаться полного достоверного периода.
points.append((event['midpoint'], event['value'])) # Угол вычисляется по восстановленным средним уровням, а не по мгновенным коммутациям.
del points[:-2] # Ограниченная история сохраняет постоянный расход памяти на длинных записях.
if event['field'] is not None: # Межфазные углы требуют наблюдавшихся полных электрических периодов.
field = event['field'] # Результат содержит последнее восходящее пересечение и частоту данной фазы.
self.fields[channel] = (field['end'], self.rate / field['frequency']) # Период хранится в отсчётах для вычисления сдвига в градусах.
if any(len(self.points.get(ch, [])) < 2 for ch in range(3)): # Пространственный вектор нельзя восстановить из неподключённых или ещё не заполненных A/B/C.
return None # Начальный участок записи не получает ложного θ.
sample = min(self.points[ch][-1][0] for ch in range(3)) # Общая точка не выходит за наблюдавшееся время ни одной фазы.
if sample <= max(self.points[ch][0][0] for ch in range(3)) or self.last_vector is not None and sample <= self.last_vector: # Требуется общий интервал и новое время, без экстраполяции остановившейся фазы.
return None # Несогласованные точки не смешиваются в один вектор.
values = [] # Три значения будут относиться к одному и тому же моменту времени.
for ch in range(3): # Порядок A/B/C определяет положительное направление вращения.
left, right = self.points[ch] # Две соседние точки ограничивают достоверный интервал данной фазы.
values.append(left[1] + (right[1] - left[1]) * (sample - left[0]) / (right[0] - left[0])) # Линейная интерполяция устраняет сдвиг, вызванный разными моментами фронтов PWM A/B/C.
alpha = (2 * values[0] - values[1] - values[2]) / 3 # Полное преобразование Кларка удаляет синфазную составляющую, включая добавку SVPWM.
beta = (values[1] - values[2]) / math.sqrt(3) # Ортогональная ось соответствует последовательности A, B, C.
self.last_vector = sample # Данный момент времени считается обработанным даже при почти нулевом векторе.
if math.hypot(alpha, beta) < 0.02: # Ниже 2% нормированного уровня направление вектора не считается определённым.
return dict(sample=sample, theta=None) # Разрыв явно запрещает соединять графиком углы через нулевую амплитуду.
return dict(sample=sample, theta=math.degrees(math.atan2(beta, alpha)) % 360) # θ выражен в электрических градусах 0…360 относительно положительной оси A.
def differences(self, sample): # Положительный результат A→B означает запаздывание фазы B относительно A.
angles = {} # Отсутствующая пара не получает выдуманный сдвиг 120 градусов.
for first, second in ((0, 1), (1, 2), (2, 0)): # Измеряются все три запрошенные пары независимо.
if first not in self.fields or second not in self.fields: # Каждой фазе пары нужен хотя бы один полный измеренный период.
continue # Две подключённые фазы всё ещё могут дать одну доступную пару.
a, b = self.fields[first], self.fields[second] # Последние одноимённые пересечения задают фазовые часы двух огибающих.
if any(sample < item[0] or sample - item[0] > 2 * item[1] for item in (a, b)): # После двух периодов без нового измерения прежний сдвиг становится устаревшим.
continue # Остановившийся или пропавший вход не сохраняет старое числовое значение.
angle = 360 * ((sample - a[0]) / a[1] - (sample - b[0]) / b[1]) # Разность фазовых часов выражает запаздывание второй огибающей в градусах.
angles['ABC'[first] + '→' + 'ABC'[second]] = (angle + 180) % 360 - 180 # Знаковый диапазон −180…180 различает запаздывание и опережение, включая обратную последовательность фаз.
return angles # Результаты рассчитываются из записи и не предполагают заранее правильную трёхфазную систему.
@@ -0,0 +1,39 @@
"""Потоковый CIC-декодер однобитного сигма-дельта модулятора без зависимостей от GUI.""" # Ядро одинаково работает в DSView и автономных анализаторах.
class SigmaDeltaFilter: # Каскад интеграторов и гребенчатых звеньев реализует фильтры sinc первого, второго и третьего порядка.
def __init__(self, decimation=256, order=3, invert=False): # Децимация задаётся числом тактов на результат; 256 и sinc3 являются начальными настройками анализатора.
if isinstance(decimation, bool) or not isinstance(decimation, int) or not 1 <= decimation <= 65536: # Ограничение 65536 тактов удерживает максимальное усиление sinc3 в пределах 49 бит.
raise ValueError('Decimation must be an integer from 1 to 65536.') # Дробные и неположительные окна не определяют допустимую децимацию.
if isinstance(order, bool) or not isinstance(order, int) or order not in (1, 2, 3): # Число интеграторов должно совпадать с числом гребенчатых звеньев.
raise ValueError('Filter order must be 1, 2 or 3.') # Неизвестный порядок нельзя незаметно заменять sinc3.
self.decimation = decimation # Каждый результат появляется после этого числа принятых битов.
self.order = order # Порядок также определяет длительность начального установления фильтра.
self.invert = bool(invert) # Инверсия применяется к битам до фильтрации и меняет знак восстановленного сигнала.
self.gain = decimation ** order # Усиление CIC при постоянном входе равно R в степени порядка при дифференциальной задержке один.
self.mask = (1 << self.gain.bit_length()) - 1 # Модуль степени двойки строго больше усиления сохраняет точный результат при переполнениях интеграторов.
self.integrators = [0] * order # Нулевое начальное состояние не предполагает неизвестную предысторию захвата.
self.delays = [0] * order # Каждый гребенчатый каскад хранит свой предыдущий вход на пониженной частоте.
self.phase = 0 # Счётчик внутри окна исключает накопление дробной ошибки децимации.
self.blocks = 0 # Первые order минус один результатов относятся к установлению и не выдаются пользователю.
def feed(self, bit): # Принимается ровно один уровень DATA на выбранном фронте CLK.
if bit not in (0, 1): # Служебные значения неподключённых каналов sigrok не являются цифровыми уровнями.
raise ValueError('Sigma-delta data must be a logic level 0 or 1.') # Ошибка назначения входов не должна превращаться в правдоподобную амплитуду.
value = int(bit) ^ self.invert # Униполярное представление позволяет нормировать результат без знакового переполнения.
for stage in range(self.order): # Последовательно обновляем интеграторы на полной частоте модулятора.
self.integrators[stage] = (self.integrators[stage] + value) & self.mask # Модульная арифметика ограничивает память независимо от длительности записи.
value = self.integrators[stage] # Следующий интегратор получает уже обновлённый выход предыдущего.
self.phase += 1 # Считаем только фактически захваченные тактовые фронты.
if self.phase < self.decimation: # Между границами децимации выходной отсчёт ещё не готов.
return None # Неполные окна не дополняются вымышленными битами.
self.phase = 0 # Новый блок начинается со следующего реального бита.
for stage in range(self.order): # Гребенчатые звенья выполняются только на выходной частоте.
previous = value # Задержка сохраняет вход текущего звена, а не его разность.
value = (value - self.delays[stage]) & self.mask # Разности устраняют накопление интеграторов, включая их модульные переполнения.
self.delays[stage] = previous # Это состояние используется через R входных тактов.
self.blocks = min(self.order, self.blocks + 1) # Счётчик установления насыщается и не растёт вместе с записью.
if self.blocks < self.order: # Для sincN пропускаем первые N минус один выходных блоков с неизвестной предысторией.
return None # Частично установившийся фильтр не выдаёт ложное смещение постоянного сигнала.
code = 2 * value - self.gain # Биполярный код принимает диапазон от минус R**N до плюс R**N без привязки к конкретному АЦП.
return {'code': code, 'gain': self.gain, 'value': code / self.gain, 'density': value / self.gain} # Нормированная амплитуда и фильтрованная плотность единиц сохраняют точный масштаб CIC.
File diff suppressed because one or more lines are too long
+110
View File
@@ -0,0 +1,110 @@
"""Тонкий Python-порт общей CAN-карты UKSVEP на STM32F103RC.""" # Форматы, GPIO и расчёт неисправностей принадлежат C99 templates.
from __future__ import annotations # Аннотации остаются совместимыми с исходниками Windows 7.
import ctypes as ct # Передаёт скаляры и буферы единственной нативной реализации.
from dataclasses import dataclass # Представляет готовые результаты C без протокольных вычислений.
from .native import get_native_protocol, NativeProtocolUnavailable # Использует штатный поиск и жизненный цикл общей DLL.
@dataclass(frozen=True) # Метаданные одинаковы для всех представлений GUI.
class Field: # Значения читаются из package.h-совместимой таблицы C.
address: int # Нулевой индекс регистра modbus[128].
maximum: int # Верхняя граница ввода в исходных единицах.
initial: int # Значение предпросмотра, которое ещё не принято от платы.
flags: int # 1 — параметр, 2 — команды, 4 — эмулируемое состояние.
name: str # Общая подпись UTF-8 из C-профиля.
@dataclass(frozen=True) # Пакет отделён от физического CAN-драйвера.
class Frame: # Готовые байты нельзя изменять через поля виджета после отправки.
can_id: int # Полный 29-битный ID без аппаратного бита IDE.
data: bytes # Восемь байтов payload, построенных C.
@dataclass(frozen=True) # Снимок содержит только реально присутствующие слова.
class Packet: # Источник пакета отделяет ответ ВЭП от команды управляющего узла.
from_device: bool # Истина означает ID данных ВЭП, а не собственное эхо команд.
values: tuple[tuple[int, int], ...] # Пары адрес/значение, разрешённые маской присутствия C.
class UksvepCan: # Каждая панель использует общее нативное ядро без собственного codec.
def __init__(self): # Привязывает только дополнительные экспорты профиля UKSVEP.
self.lib = get_native_protocol().lib # Загрузка DLL и общий ABI уже проверены штатным портом.
signatures = { # Типы описывают ABI, а не байтовый порядок wire-протокола.
"uksvep_can_id": ([ct.c_uint, ct.c_uint], ct.c_uint32), # Возвращает маршрут для физического адреса и роли.
"uksvep_can_encode": ([ct.c_uint, ct.c_uint, ct.c_uint, ct.POINTER(ct.c_uint16), ct.c_size_t, ct.POINTER(ct.c_uint8), ct.c_size_t], ct.c_size_t), # Кодирует до трёх слов в буфер caller.
"uksvep_can_decode": ([ct.c_uint, ct.c_uint32, ct.POINTER(ct.c_uint8), ct.c_size_t, ct.POINTER(ct.c_uint16), ct.c_size_t], ct.c_int), # Возвращает плоский массив пар из C.
"uksvep_field_count": ([], ct.c_uint), # Позволяет получить все поля без таблицы дублей Python.
"uksvep_field_info": ([ct.c_uint, ct.c_uint, ct.POINTER(ct.c_uint16), ct.c_size_t], ct.c_int), # Возвращает четыре скаляра метаданных.
"uksvep_field_name": ([ct.c_uint], ct.c_char_p), # C владеет постоянной строкой UTF-8.
"uksvep_input_name": ([ct.c_uint], ct.c_char_p), # Назначения GPIO остаются общими с профилем.
"uksvep_command_name": ([ct.c_uint], ct.c_char_p), # Пустое имя обозначает резервный бит R127.
"uksvep_evaluate_inputs": ([ct.c_uint16, ct.c_uint16, ct.c_uint16, ct.POINTER(ct.c_uint16), ct.c_size_t], None), # Рассчитывает R1/R2 для эмуляции по алгоритму C.
} # Символы добавлены без изменения существующего ABI версии 1.
try: # Старая DLL должна давать понятную ошибку вместо тихого Python fallback.
for name, (arguments, result) in signatures.items(): # Каждый экспорт получает точную сигнатуру до первого вызова.
function = getattr(self.lib, name) # Отсутствие профиля в старой DLL обнаруживается здесь.
function.argtypes = arguments # ctypes не угадывает размер указателей и целых.
function.restype = result # Сохраняет 32-битные ID и указатели строк на 64-битном хосте.
except AttributeError as error: # Формат wire не реализуется заново в аварийной ветви.
raise NativeProtocolUnavailable("В общей DLL нет профиля UKSVEP. Пересоберите setprotocol.dll из templates.") from error # Поясняет требуемое действие пользователю.
@staticmethod # Валидация Python-аргумента предотвращает обрезание ctypes до C-типа.
def _device(value): # Адрес хранится целым числом из четырёх перемычек.
if not isinstance(value, int) or not 0 <= value <= 15: # Нельзя молча адресовать соседний узел по младшим битам.
raise ValueError("DeviceID ВЭП должен быть 0…15") # Ошибка возникает до передачи CAN.
return value # Валидный скаляр далее проверяется также C-профилем.
def can_id(self, device, from_device=False): # Доверяет вычисление маршрута общему C-профилю.
return int(self.lib.uksvep_can_id(self._device(device), bool(from_device))) # Python не повторяет базу и смещение идентификатора.
def fields(self, device=0): # Получает подтверждённые поля выбранного ВЭП.
device = self._device(device) # Проверяет адрес до создания начальных значений Mode.
result = [] # Список содержит только готовые метаданные ABI.
for index in range(self.lib.uksvep_field_count()): # Размер карты также принадлежит C.
raw = (ct.c_uint16 * 4)() # Caller владеет явно ограниченным выходным буфером.
if not self.lib.uksvep_field_info(index, device, raw, len(raw)): # Проверяет результат вызова вместо чтения частично готового массива.
raise ValueError("Не удалось получить карту UKSVEP") # Ошибка не заменяется выдуманными defaults.
name = self.lib.uksvep_field_name(index).decode("utf-8") # Декодирует только строковое представление общей подписи.
result.append(Field(*map(int, raw), name)) # Не меняет единицы и адреса, возвращённые C.
return tuple(result) # Карта не изменяется виджетами во время работы.
def names(self, kind): # Читает общие назначения входов либо команд.
function = self.lib.uksvep_input_name if kind == "inputs" else self.lib.uksvep_command_name # Выбор относится к категории отображения, а не к codec.
return tuple(function(bit).decode("utf-8") for bit in range(16)) # Резервные позиции остаются пустыми строками.
def frames(self, device, address, values, from_device=False): # Делит длинный снимок на штатные вызовы трёхсловного C-кодировщика.
device = self._device(device) # Неверный адрес не может быть обрезан ctypes.
values = tuple(values) # Фиксирует входной снимок до первой физической записи.
if not values or not isinstance(address, int) or not 0 <= address < 128 or len(values) > 128 - address: # Проверяет Python-диапазон до преобразования unsigned.
raise ValueError("Выберите непустой диапазон R0…R127") # Ошибка обнаруживается до формирования первого пакета.
if any(not isinstance(value, int) or not 0 <= value <= 65535 for value in values): # Не обрезает отрицательные или переполненные слова.
raise ValueError("Значения ВЭП должны быть uint16: 0…65535") # Поле пользователя не должно превращаться в другое число на проводе.
result = [] # Все байты создаются до передачи, чтобы не отправить половину неверного снимка.
for offset in range(0, len(values), 3): # Ограничение трёх слов задано существующим историческим форматом.
part = values[offset:offset + 3] # UI-порт только разбивает контейнер, не строит маску/endian.
source = (ct.c_uint16 * len(part))(*part) # Преобразует значения в явно типизированный вход C.
target = (ct.c_uint8 * 8)() # Payload classic CAN ограничен восемью байтами.
size = self.lib.uksvep_can_encode(device, bool(from_device), address + offset, source, len(part), target, len(target)) # Все поля wire заполняет C99.
if size != 8: # Валидный UKSVEP кадр обязан иметь полный DLC.
raise ValueError("C-профиль отклонил регистры UKSVEP") # Ошибка не скрывается частичной передачей.
result.append(Frame(self.can_id(device, from_device), bytes(target))) # Возвращает готовый транспортно-независимый кадр.
return tuple(result) # Физический writer принадлежит существующему BusTab.
def decode(self, device, can_id, data): # Вызывает общий decoder с ограниченными буферами.
device = self._device(device) # Проверяет адрес до входа в ctypes.
if not isinstance(can_id, int) or not 0 <= can_id <= 0x1FFFFFFF: # Исключает знаковое переполнение и биты вне extended ID.
return None # Такое событие не принадлежит допустимому CAN-профилю.
data = bytes(data) # Фиксирует входной payload на время вызова C.
source = (ct.c_uint8 * len(data)).from_buffer_copy(data) # Caller владеет памятью до завершения синхронного разбора.
target = (ct.c_uint16 * 8)() # Восемь скаляров вмещают направление и три адресные пары.
count = self.lib.uksvep_can_decode(device, can_id, source, len(data), target, len(target)) # C выбирает ID, DLC, маску и границы карты.
if count < 0: # Повреждённый пакет выбранного ВЭП не считается чужим.
raise ValueError("Повреждённый CAN-кадр UKSVEP: DLC, маска или адрес") # UI может увеличить счётчик отказов RX.
if count == 0: # Пакет другого адреса не меняет снимок.
return None # Общая шина продолжает обрабатывать его независимо.
pairs = tuple((int(target[2 + index * 2]), int(target[3 + index * 2])) for index in range(count)) # Переводит плоский ABI в готовые пары без повторного endian-разбора.
return Packet(bool(target[0]), pairs) # Направление не позволяет выдать эхо TX за ответ платы.
def evaluate(self, inputs, alarm_mask, error_mask): # Предоставляет явный расчёт состояния эмулируемого ВЭП.
values = (inputs, alarm_mask, error_mask) # Передаёт только исходные слова C-алгоритму ReadEnteres.
if any(not isinstance(value, int) or not 0 <= value <= 65535 for value in values): # Защищает параметры от неявного uint16 обрезания.
raise ValueError("Входы и маски должны быть uint16") # Неверный ввод не превращается в правдоподобную аварию.
result = (ct.c_uint16 * 2)() # Буфер содержит R1 и R2, вычисленные C.
self.lib.uksvep_evaluate_inputs(*values, result, len(result)) # Python не повторяет особые случаи заряд/разряд/24 В.
return tuple(map(int, result)) # Результат используется только в исходящем эмулируемом снимке.
+1 -1
View File
@@ -473,7 +473,7 @@ def command_can_interface(name: str) -> str: # Расширение общег
return 'N' + name # Выбор интерфейса не включает драйвер и не передаёт кадры автоматически.
def command_bitrate(kbps: str | int) -> str: # Общая команда битрейта используется ESP и Zynq без локальных копий.
def command_bitrate(kbps: str | int) -> str:
"""Скорость шины CAN, кбит/с."""
return "S%s" % kbps
File diff suppressed because one or more lines are too long
+7
View File
@@ -44,3 +44,10 @@ CANDLE_G474_BOOT = { # Профиль воспроизводит CAN_BOOTLOADER
"device_type": 7, "device": 15, "product": 0, "hardware": 0, # Идентичность ProtoCAN BOOT: Type 7, Device 15, Product 0 и аппаратная ревизия 0.
"can_bitrate": 500_000, "uart_baud": 512_000, # Classic CAN работает на 500 кбит/с, USART1 PA9/PA10 — на 512000 бод, 8N1.
} # Адреса и скорости относятся только к CANdle G474VET6 и не подменяют профиль USB BOOT G474RB.
UKSVEP_F103RC_BOOT = { # Профиль отдельного boot-проекта UKSVEP_ICE_22220.6 согласован с ports/stm32f103rc-uksvep/boot_profile.h.
"app_base": 0x08008000, "app_size": 94 * 1024, # Начальные 32 КиБ заняты BOOT; приложение заканчивается перед существующей EEPROM-страницей 0x0801F800.
"stack_base": 0x20000000, "stack_end": 0x2000C000, # DS5792 определяет для STM32F103RC 48 КиБ SRAM и восьмибайтовое выравнивание начального MSP.
"device_type": 5, "product": 0x5606, "hardware": 1, # DeviceID задают активные нулём перемычки PA6/PA7/PC4/PC5; Product отличает ВЭП от других STM32.
"can_bitrate": 250_000, "uart_baud": 115_200, # Загрузчик UKSVEP использует CAN PA11/PA12 и USART1 PA9 TX/PA10 RX на 115200 бод.
} # Single-slot обновление сохраняет параметры; для запуска требуется постоянный manifest и CRC образа.
File diff suppressed because one or more lines are too long
+4 -1
View File
@@ -79,6 +79,7 @@ class SlcanAdapter(QObject):
frame_received = Signal(int, bytes)
capture_received = Signal(int, bytes, bool, bool)
connection_lost = Signal(str)
protocol_error = Signal(str) # Сообщает явный отказ BEL вне настройки; принятие байтов COM не подтверждает выполнение команды адаптером.
fd_received = Signal(int, bytes, bool, int, int) # Отдельный маршрут FD исключает ошибочный разбор кадра как Classic CAN.
def __init__(self, parent: QObject | None = None) -> None:
@@ -222,6 +223,8 @@ class SlcanAdapter(QObject):
if separator == 7: # ASCII BEL имеет код 7 и сообщает об отклонении последней команды.
if self._waiting_command and self._command_reply is None: # Принимаем только первый ответ текущего этапа настройки.
self._command_reply = False # Сохраняем отказ для проверки в последовательном обмене командами.
elif not self._waiting_command and self._query_prefix is None: # Отказ диагностического запроса не приписывается передаче CAN; установку режима обслуживает собственное ожидание.
self.protocol_error.emit("SLCAN: адаптер вернул отказ BEL; проверьте состояние CAN и очередь передачи") # BEL не содержит идентификатора кадра и причины, поэтому не выдаём его за доказательство отсутствия CAN ACK.
continue # BEL не должен приклеиваться к следующему CAN-кадру.
if self.options.fd and raw[:1] in (b"d", b"D", b"b", b"B") and self._receive_fd(raw): # Только корректная FD-строка забирается отдельным маршрутом; версия CANable может начинаться с HEX-буквы.
continue # Даже FD с данными до восьми байтов не меняет тип при передаче подписчикам.
@@ -286,7 +289,7 @@ class SlcanAdapter(QObject):
lines = [] # Ответы V и N читают сведения, F дополнительно сбрасывает индикатор ошибки согласно Lawicel.
canable = self.options.profile != "lawicel" # CANable возвращает V без буквенного префикса, а регистр E иногда без завершающего CR.
if canable: # Отсутствие подтверждения настроек сообщается независимо от успешного чтения версии.
lines.append(f"Профиль {self.options.profile}: команды конфигурации без ACK/NACK; применение настроек не подтверждается прошивкой.") # Диагностика не превращает ответ V в подтверждение предыдущих M/A/Y.
lines.append(f"Профиль {self.options.profile}: команды настройки по COM без ответов CR/BEL; это не аппаратное подтверждение ACK на шине CAN.") # Отделяет отсутствие ответов протокола SLCAN от аппаратного ACK, который активный CAN-контроллер формирует самостоятельно.
for command in (("V", "E") if canable else ("V", "N", "F")): # Набор фиксирован спецификацией выбранной прошивки.
self._read() # Сначала выдаём уже накопленные CAN-кадры, не смешивая их с ответом запроса.
self._query_prefix, self._query_reply = (b"" if canable else command.encode("ascii")), None # CANable не использует префикс V в строке версии.
+102
View File
@@ -0,0 +1,102 @@
"""Асинхронный J-Link-порт: backup, запись, readback, проверка и отдельный reset/run.""" # Внешний процесс не блокирует цикл событий SETGUI.
from pathlib import Path # Артефакты одной операции хранятся вместе в отдельном каталоге.
import re # Итоговый запуск подтверждается выполненной командой g, а не одним кодом возврата.
import tempfile # Уникальный каталог исключает перезапись предыдущей резервной копии.
try: # Общая библиотека работает с современным Qt и существующими сборками Windows 7.
from PySide6.QtCore import QObject, QProcess, QTimer, Signal # QProcess обслуживает stdout, завершение и отмену без потоковой блокировки.
except ImportError: # PySide2 сохраняет тот же публичный контракт сигналов этого порта.
from PySide2.QtCore import QObject, QProcess, QTimer, Signal # Никакой зависимости от внутреннего устройства SETGUI здесь нет.
from set_devices.stm32_jtag import encode_hex, programming_script, start_script, verify_readback # Вся подготовка и проверка сосредоточены в переносимом модуле templates.
class Stm32JtagProgrammer(QObject): # Владеет одной аппаратной операцией до полного завершения обоих этапов.
progress = Signal(int, str) # Основная форма показывает этапы рядом с существующей полосой прогресса.
finished = Signal(bool, str) # Единственный итог освобождает форму также при ошибке запуска executable.
log = Signal(str) # Полный вывод Commander доступен в терминале и сохраняется на диск.
def __init__(self, parent=None): # Создание порта не открывает программатор и не обращается к USB.
super().__init__(parent) # Время жизни порта соответствует его Qt-владельцу.
self._process = None # None означает отсутствие запущенного Commander.
self._active = False # Флаг защищает от повторного finished после errorOccurred и finished одного процесса.
self._timer = QTimer(self) # Ограничивает зависшую внешнюю утилиту без ожидания в GUI.
self._timer.setSingleShot(True) # На один этап приходится один предельный тайм-аут.
self._timer.timeout.connect(lambda: self._stop('Истекло время ожидания J-Link; проверьте журнал')) # Остановка не объявляет частично записанный образ успешным.
@property # Форма может проверить занятость перед повторным нажатием или закрытием.
def active(self): # Состояние охватывает программирование, независимую проверку и reset/run.
return self._active # Между двумя процессами нельзя начать конкурирующую запись.
def start(self, executable, options, segments, backup_root): # Принимает уже проверенный неизменяемый план, подтверждённый пользователем.
if self._active: raise ValueError('J-Link уже выполняет операцию') # Второй запуск не подменяет исходный путь backup и цель MCU.
options.validate() # Защищает прямые вызовы порта вне стандартной формы.
self._options, self._segments, self._executable = options, segments, str(executable) # Копирует параметры текущей операции до запуска QProcess.
self._active, self._stop_reason = True, '' # Обозначает владение программатором и отсутствие запроса отмены.
try: # Ошибки каталога и снимка должны завершать операцию штатным сигналом формы.
root = Path(backup_root).resolve() # Выбранный каталог не зависит от текущей рабочей папки процесса GUI.
root.mkdir(parents=True, exist_ok=True) # Создаёт только папку артефактов пользователя, без установки инструментов.
self.folder = Path(tempfile.mkdtemp(prefix='stm32-jtag-', dir=root)) # Предыдущие backup остаются нетронутыми даже при повторной неудачной попытке.
(self.folder / 'image.hex').write_text(encode_hex(segments), encoding='utf-8') # Фиксирует проверенные адресованные данные, исключая изменение исходника во время записи.
self.log.emit(f'Артефакты J-Link и резервная копия: {self.folder}') # Путь известен оператору до первого аппаратного действия.
self.progress.emit(0, 'Резервная копия, запись и чтение Flash') # Число отражает этап, поскольку Commander сам выводит ход программирования в терминал.
self._launch('program', programming_script(self.folder, options)) # Сначала исполняется только этап с остановленным ядром и двумя SaveBin.
except (OSError, ValueError) as error: self._finish(False, str(error)) # Отказ файловой системы не оставляет интерфейс заблокированным.
def _launch(self, stage, commands): # Каждый этап использует новый QProcess и собственный проверяемый command file.
self._stage, self._output = stage, '' # Вывод текущего этапа нужен также для подтверждения факта команды запуска.
script = self.folder / (stage + '.jlink') # Точный выполненный сценарий сохраняется рядом с backup.
script.write_text(commands, encoding='utf-8') # UTF-8 без BOM совместим с установленным Commander.
process = QProcess(self) # QObject-владелец удерживает процесс до обработки сигнала finished.
self._process = process # Отмена и тайм-аут относятся только к текущему этапу.
process.setProcessChannelMode(QProcess.MergedChannels) # Ошибки stderr сохраняются в том же порядке, что служебные сообщения stdout.
process.readyReadStandardOutput.connect(self._read_output) # Строки отображаются по мере поступления без waitForFinished.
process.errorOccurred.connect(self._process_error) # Отсутствующий executable и невозможность запуска дают понятный итог.
process.finished.connect(self._process_finished) # Только штатное завершение запускает независимую проверку и следующий этап.
self._timer.start(300000) # Пять минут на этап покрывают медленный JTAG; GUI продолжает обрабатывать отмену.
process.start(self._executable, self._options.arguments() + ['-Log', str(self.folder / (stage + '-dll.log')), '-CommanderScript', str(script)]) # Путь, серийный номер и файл передаются отдельными аргументами без командной оболочки.
def _read_output(self): # Вызывается также при finished, чтобы не потерять последний фрагмент stdout.
if self._process is None: return # Сигнал уже освобождённого этапа не должен обращаться к новому процессу.
chunk = bytes(self._process.readAllStandardOutput()).decode('utf-8', errors='replace').replace('\b', '') # Управляющие backspace процентов Commander не ломают текстовый терминал.
self._output += chunk # Полный вывод сохраняется для анализа результата и диагностики ошибки.
if chunk: self.log.emit(chunk.rstrip()) # Отображает сообщения сразу, включая отсутствие питания и занятой J-Link.
def _process_error(self, error): # FailedToStart не сопровождается finished и требует самостоятельного завершения.
if self._active and error == QProcess.FailedToStart: self._finish(False, 'Не удалось запустить J-Link: ' + self._process.errorString()) # Не оставляет активный таймер после неверного пути executable.
def _process_finished(self, code, status): # Последовательно выполняет readback-проверку и отдельный reset/run.
if not self._active: return # Игнорирует повторное уведомление ранее завершённой ошибки.
self._timer.stop() # Проверка локальных файлов не должна срабатывать как тайм-аут уже завершённого Commander.
self._read_output() # Забирает остаток вывода перед сохранением журнала.
try: # Все ошибки проверки и записи журнала превращаются в один finished(False).
(self.folder / (self._stage + '.log')).write_text(self._output, encoding='utf-8') # Читаемый журнал содержит именно тот вывод, который видел оператор.
process, self._process = self._process, None # Старый QObject больше не считается владельцем текущего этапа.
process.deleteLater() # Удаление выполняется циклом Qt после выхода из обработчика finished.
if self._stop_reason: raise ValueError(self._stop_reason) # Отмена никогда не переходит к командам reset/run.
if code != 0 or status != QProcess.NormalExit: raise ValueError(f'J-Link завершился с ошибкой {code}; журнал: {self.folder}') # Успех требует нормального завершения внешней утилиты.
if self._stage == 'program': # Даже код ноль не подтверждает, что интерактивное подключение реально дошло до записи.
before, after = (self.folder / 'before.bin').read_bytes(), (self.folder / 'after.bin').read_bytes() # Отсутствующий SaveBin автоматически исключает ложный успех.
verify_readback(self._segments, self._options, before, after) # Проверяет реальные байты записанных диапазонов и защищённую страницу UKSVEP.
self.progress.emit(90, 'Flash проверена, сброс и запуск STM32') # Переход к запуску разрешён только после независимой проверки обоих снимков.
self._launch('run', start_script(self._options)) # Новый Commander больше не стирает и не программирует Flash.
else: # Второй этап должен дойти до явного возобновления исполнения ядра.
if not re.search(r'J-Link>\s*g\s*(?:\r?\n|$)', self._output): raise ValueError('J-Link не подтвердил выполнение команды запуска; проверьте журнал') # Преждевременный выход с кодом ноль не превращается в сообщение о работающем MCU.
self.progress.emit(100, 'Запись и проверка завершены, выполнен reset/run') # Проценты завершаются только после обоих этапов.
self._finish(True, f'STM32 прошит через {self._options.interface}; Flash проверена, reset/run выполнен. Backup: {self.folder / "before.bin"}') # Итог содержит доступную для восстановления резервную копию.
except (OSError, ValueError) as error: self._finish(False, str(error)) # При отказе проверки ядро не запускается новым этапом.
def cancel(self): # Вызывается штатной кнопкой отмены формы прошивки.
self._stop('J-Link: операция отменена; частичная Flash требует повторной прошивки, резервная копия сохранена в каталоге операции') # Не обещает целый образ после прерывания аппаратной записи.
def _stop(self, reason): # Общая остановка обслуживает кнопку отмены и предельный тайм-аут.
if not self._active: return # Повторная отмена завершённого порта ничего не меняет.
self._stop_reason = reason # finished процесса должен сохранить причину вместо сообщения об обычном ненулевом коде.
if self._process is not None: self._process.kill() # Останавливает только запущенный этим портом Commander, не затрагивая Keil и другие программаторы.
else: self._finish(False, reason) # Ошибка между этапами также освобождает форму без дополнительного процесса.
def _finish(self, success, message): # Единственная точка завершения защищает форму от двойных уведомлений.
if not self._active: return # Второй сигнал ошибки того же QProcess не открывает повторное окно.
self._active = False # Новая операция разрешается только после окончательного результата текущей.
self._timer.stop() # Поздний тайм-аут не должен отменить следующий пользовательский запуск.
if self._process is not None: self._process.deleteLater() # FailedToStart не использует общий обработчик finished.
self._process = None # Освобождает ссылку на завершённый QObject.
self.finished.emit(success, message) # Владелец разблокирует форму и покажет итог стандартным механизмом SETGUI.
@@ -0,0 +1,96 @@
"""Общие настройки STM32/J-Link для встраивания в существующий экран прошивки.""" # SETGUI предоставляет лишь выбранный образ и свой терминал.
from pathlib import Path # Проверяет путь внешнего инструмента до подтверждения аппаратной операции.
try: # Поддерживает обе используемые SETGUI ветви Qt без зависимости от gui_desktop.
from PySide6.QtCore import QCoreApplication, QSettings, QStandardPaths, Signal # Настройки и каталог backup принадлежат текущему приложению.
from PySide6.QtWidgets import QWidget, QFormLayout, QHBoxLayout, QVBoxLayout, QLineEdit, QComboBox, QSpinBox, QCheckBox, QPushButton, QFileDialog, QLabel # Использует обычные виджеты, доступные в Qt 5 и Qt 6.
except ImportError: # Старые Windows-сборки сохраняют тот же публичный интерфейс панели.
from PySide2.QtCore import QCoreApplication, QSettings, QStandardPaths, Signal # Не навязывает основному приложению версию Qt.
from PySide2.QtWidgets import QWidget, QFormLayout, QHBoxLayout, QVBoxLayout, QLineEdit, QComboBox, QSpinBox, QCheckBox, QPushButton, QFileDialog, QLabel # Набор контролов одинаков для обеих реализаций.
from set_devices.stm32_jtag import JLinkOptions, find_jlink, prepare_image # Общий план выполняет все проверки MCU, адресов и manifest до записи.
from .stm32_jtag import Stm32JtagProgrammer # Порт QProcess отвечает за журнал, отмену и оба этапа операции.
class Stm32JtagPanel(QWidget): # Встраивается в FirmwareTab вместо UART/CAN-настроек выбранного режима.
progress = Signal(int, str) # Перенаправляет этапы в полосу прогресса основной формы.
finished = Signal(bool, str) # Результат обрабатывается тем же методом, что остальные загрузчики.
log = Signal(str) # Вывод J-Link направляется в существующий терминал прошивки.
def __init__(self, parent=None, settings=None): # Явное хранилище позволяет изолировать настройки в тестах и других приложениях.
super().__init__(parent) # Панель следует обычному времени жизни владельца QWidget.
self.settings = settings if settings is not None else QSettings() # Не подменяет тестовый профиль глобальным пользовательским реестром.
self.programmer = Stm32JtagProgrammer(self) # Создание формы само по себе не подключает USB и не запускает Commander.
self.programmer.progress.connect(self.progress.emit) # Прогресс проходит через общий публичный интерфейс панели.
self.programmer.finished.connect(self.finished.emit) # Ошибки и успех освобождают внешнюю форму одинаковым способом.
self.programmer.log.connect(self.log.emit) # Терминал получает и служебные строки, и сообщения SEGGER.
if QCoreApplication.instance() is not None: QCoreApplication.instance().aboutToQuit.connect(self.programmer.cancel) # Закрытие приложения не оставляет скрытый процесс программирования без владельца.
root = QVBoxLayout(self) # Все настройки образуют одну колонку внутри существующей левой панели.
root.setContentsMargins(0, 0, 0, 0) # Внешние отступы уже заданы формой прошивки.
hint = QLabel('J-Link через JTAG или SWD. Доступ к BOOT0 не требуется. Перед записью сохраняется полная резервная копия Flash.') # Оператор сразу видит назначение режима и способ первого программирования.
hint.setWordWrap(True) # Описание помещается в узкую колонку рядом с терминалом.
root.addWidget(hint) # Пояснение располагается над параметрами подключения.
form = QFormLayout() # Подписи не смешиваются с UART-полями других загрузчиков.
self.executable = QLineEdit(str(self._value('executable', find_jlink()))) # Позволяет использовать SEGGER из Keil либо отдельную установленную версию.
executable_row = QHBoxLayout() # Кнопка выбора остаётся рядом с полным путём инструмента.
executable_row.addWidget(self.executable) # Введённый путь передаётся QProcess отдельным аргументом.
executable_button = QPushButton('Обзор…') # Поиск установленного инструмента не требует скачивания и установки.
executable_button.clicked.connect(self._choose_executable) # Диалог выбирает только путь, без запуска программатора.
executable_row.addWidget(executable_button) # Кнопка сохраняет естественную компактную ширину.
form.addRow('J-Link Commander', executable_row) # Имя executable явно отделено от названия MCU.
self.device = QComboBox() # Стандартные имена подходят базе устройств установленного Commander.
self.device.setEditable(True) # Другой STM32 можно указать вручную вместе с фактическим объёмом Flash.
self.device.addItems(['STM32F103RC', 'STM32F103C8', 'STM32F103ZE', 'STM32F407VE', 'STM32G474VE']) # Список охватывает используемые проектами templates семейства.
self.device.setCurrentText(str(self._value('device', 'STM32F103RC'))) # По умолчанию выбирается текущая плата, ранее проверенная через JTAG.
form.addRow('Микроконтроллер', self.device) # Полное имя цели видно до подтверждения записи.
self.interface = QComboBox() # Интерфейс не зависит от UART-скорости верхней панели SETGUI.
self.interface.addItems(['JTAG', 'SWD']) # Оба варианта используют J-Link, а не встроенный ROM Boot.
self.interface.setCurrentText(str(self._value('interface', 'JTAG'))) # Исходное подключение текущей платы выполнено по JTAG.
form.addRow('Интерфейс', self.interface) # Пользователь видит выбранный физический способ подключения.
self.serial = QLineEdit(str(self._value('serial', ''))) # Не закрепляет серийный номер чужого программатора в общей библиотеке.
self.serial.setPlaceholderText('Необязательно, если подключён один J-Link') # Несколько программаторов можно однозначно различить по серийному номеру.
form.addRow('Серийный номер J-Link', self.serial) # Поле соответствует штатному параметру Commander -USB.
self.speed = QSpinBox() # Частота задаётся числом с явными единицами.
self.speed.setRange(1, 50000) # Допускает медленную плату и быстрые поддерживаемые подключения.
self.speed.setValue(int(self._value('speed', 1000))) # Проверенная начальная частота 1000 кГц подходит текущему JTAG.
self.speed.setSuffix(' кГц') # Исключает смешение TCK/SWCLK со скоростью COM в бодах.
form.addRow('Частота JTAG/SWD', self.speed) # Значение передаётся напрямую в -speed.
self.flash_size = QSpinBox() # Полный объём нужен для сохранения Flash перед любым программированием.
self.flash_size.setRange(16, 4096) # Интервал покрывает поддерживаемый переносимым планом объём 16 КиБ..4 МиБ.
self.flash_size.setValue(int(self._value('flash_size', 256))) # У STM32F103RC установлены 256 КиБ внутренней Flash.
self.flash_size.setSuffix(' КиБ') # Длина SaveBin вычисляется из этого значения в байтах.
form.addRow('Объём Flash', self.flash_size) # Для другого MCU оператор должен указать его фактический объём.
self.uksvep = QCheckBox('UKSVEP F103RC: сохранить параметры и обновить CRC приложения') # Явный профиль разрешает специфические адреса manifest и регистров BKP.
self.uksvep.setChecked(self.settings.value('firmware/stm32-jtag/uksvep', True, type=bool)) # Выбор сохраняется независимо от общего имени MCU.
self.uksvep.setToolTip('Для другой платы снимите этот флажок. UKSVEP принимает полный app с 0x08008000 или установочный HEX boot+app.') # Объясняет действие профиля при осмысленном выборе оператором.
form.addRow('Профиль платы', self.uksvep) # Обычные STM32 не наследуют защиту и служебную запись UKSVEP автоматически.
default_backup = str(Path(QStandardPaths.writableLocation(QStandardPaths.AppLocalDataLocation)) / 'stm32-jtag') # Каталог остаётся записываемым также у установленного в Program Files приложения.
self.backup_root = QLineEdit(str(self._value('backup_root', default_backup))) # Каждая операция создаёт внутри отдельную подпапку с образами и журналами.
backup_row = QHBoxLayout() # Настройка архива не требует ручного редактирования путей Windows.
backup_row.addWidget(self.backup_root) # Полный путь остаётся доступным для копирования оператором.
backup_button = QPushButton('Обзор…') # Выбирает существующий базовый каталог резервных копий.
backup_button.clicked.connect(self._choose_backup) # Диалог не изменяет Flash и настройки программатора.
backup_row.addWidget(backup_button) # Действие располагается у соответствующего пути.
form.addRow('Резервные копии', backup_row) # Backup включён всегда и не прячется в настройках UART.
root.addLayout(form) # Готовая панель встраивается без собственной кнопки прошивки и повторной полосы прогресса.
def _value(self, name, default): # Все ключи принадлежат отдельному стабильному разделу прошивки J-Link.
return self.settings.value('firmware/stm32-jtag/' + name, default) # Прежние UART/CAN-настройки не меняются.
def _choose_executable(self): # Пользователь явно указывает уже установленный J-Link Commander.
path, _ = QFileDialog.getOpenFileName(self, 'J-Link Commander', self.executable.text(), 'J-Link (JLink.exe JLinkExe);;Все файлы (*)') # Расширение не ограничивает Linux-версию SEGGER.
if path: self.executable.setText(path) # Отмена диалога сохраняет текущий путь.
def _choose_backup(self): # Позволяет хранить backup вне каталога установки GUI и исходников.
path = QFileDialog.getExistingDirectory(self, 'Каталог резервных копий STM32', self.backup_root.text()) # Выбор каталога не запускает аппаратную операцию.
if path: self.backup_root.setText(path) # Прежний архив остаётся выбранным при отмене диалога.
def prepare(self, image): # Основная форма вызывает проверку до стандартного подтверждения записи.
executable = Path(self.executable.text().strip()) # Путь не интерпретируется как команда оболочки.
if not executable.is_file(): raise ValueError('Выберите установленный J-Link Commander (JLink.exe)') # Отказывает до блокировки формы и открытия программатора.
if not self.backup_root.text().strip(): raise ValueError('Выберите каталог резервных копий Flash') # Не сохраняет обязательный backup в непредсказуемый текущий каталог.
options = JLinkOptions(self.device.currentText().strip(), self.interface.currentText(), self.speed.value(), self.serial.text().strip(), self.flash_size.value(), self.uksvep.isChecked()) # Фиксирует все параметры одновременно с неизменяемым образом.
return str(executable), options, prepare_image(image, options), self.backup_root.text().strip() # Возвращает точный план для единственного последующего подтверждённого запуска.
def start(self, job): # Вызывается только после существующего подтверждения прошивки в FirmwareTab.
for name, widget in (('executable', self.executable), ('serial', self.serial), ('backup_root', self.backup_root)): self.settings.setValue('firmware/stm32-jtag/' + name, widget.text().strip()) # Сохраняет пути и идентификатор без доступа к остальным настройкам GUI.
for name, value in (('device', self.device.currentText()), ('interface', self.interface.currentText()), ('speed', self.speed.value()), ('flash_size', self.flash_size.value()), ('uksvep', self.uksvep.isChecked())): self.settings.setValue('firmware/stm32-jtag/' + name, value) # Следующий запуск формы восстанавливает фактическое последнее подключение.
self.programmer.start(*job) # Процесс получает уже проверенный снимок, а не повторно читает изменяемые поля формы.
+124
View File
@@ -0,0 +1,124 @@
"""Подготовка и независимая проверка J-Link-прошивки STM32 без зависимости от Qt.""" # Один план используют SETGUI и консольные клиенты templates.
from dataclasses import dataclass # Неизменяемые настройки фиксируют назначение до первой записи Flash.
from pathlib import Path # Пути передаются программатору как данные, без выполнения оболочкой.
import os # Стандартные каталоги Windows позволяют найти уже установленный J-Link.
import re # Ограничивает поля, попадающие в аргументы J-Link Commander.
import shutil # Поиск PATH сохраняет возможность использовать установленный SEGGER вне Keil.
import struct # Manifest UKSVEP содержит шесть little-endian слов uint32.
import zlib # CRC32 IEEE совпадает с валидатором production-загрузчика UKSVEP.
from firmware_image.core import HexSegment, parse_intel_hex, contiguous_image # Проверка формата и checksum использует каноническое C-ядро templates.
from .mcu_profiles import UKSVEP_F103RC_BOOT # Адрес приложения, SRAM и Product берутся из общего профиля платы.
FLASH_BASE = 0x08000000 # Внутренняя Flash STM32 отображается с этого адреса в карте памяти Cortex-M.
UKS_APP = UKSVEP_F103RC_BOOT['app_base'] # Начальные 32 КиБ UKSVEP занимает постоянный загрузчик.
UKS_PARAMS = UKS_APP + UKSVEP_F103RC_BOOT['app_size'] # Следующая страница 0x0801F800 хранит параметры и исключается из записи.
UKS_META = 0x0803F800 # Последняя страница 256-КиБ Flash содержит production-manifest, см. boot_profile.h UKSVEP.
@dataclass(frozen=True) # Значения нельзя незаметно изменить между подтверждением формы и запуском процесса.
class JLinkOptions: # Описывает конкретный STM32 и физическое подключение программатора.
device: str = 'STM32F103RC' # Имя совпадает с базой устройств J-Link; корпусный суффикс T6 не требуется.
interface: str = 'JTAG' # SWD использует тот же Commander и выбирается пользователем отдельно.
speed_khz: int = 1000 # Частота TCK/SWCLK задаётся в кГц и не связана со скоростью UART.
serial: str = '' # Пустое значение разрешает штатный выбор единственного подключённого J-Link.
flash_kib: int = 256 # Размер всей Flash задаёт длину обязательных backup и readback.
uksvep: bool = True # Только явно выбранный профиль UKSVEP включает защиту параметров и manifest.
def validate(self): # Отказ происходит до создания command file и доступа к плате.
if not re.fullmatch(r'STM32[A-Za-z0-9_]+', self.device): raise ValueError('Укажите имя STM32 из базы J-Link') # Не допускает пробелы и команды вместо идентификатора MCU.
if self.interface not in ('JTAG', 'SWD'): raise ValueError('Выберите JTAG или SWD') # Остальные интерфейсы не относятся к этой панели.
if not 1 <= self.speed_khz <= 50000: raise ValueError('Частота JTAG/SWD должна быть 1..50000 кГц') # Верхняя граница соответствует доступному диапазону формы.
if self.serial and not re.fullmatch(r'[0-9]+', self.serial): raise ValueError('Серийный номер J-Link должен содержать только цифры') # Идентификатор не должен включать дополнительные аргументы.
if not 16 <= self.flash_kib <= 4096: raise ValueError('Размер Flash должен быть 16..4096 КиБ') # Ограничивает аппаратное чтение выбранным пользователем объёмом STM32.
if self.uksvep and (self.device != 'STM32F103RC' or self.flash_kib != 256): raise ValueError('Профиль UKSVEP требует STM32F103RC и Flash 256 КиБ') # Не применяет карту F103RC и регистры BKP к другому семейству.
def arguments(self): # Возвращает отдельные аргументы QProcess без shell=True и строковой сборки команды.
self.validate() # Публичный метод безопасен также вне GUI.
args = ['-device', self.device, '-if', self.interface, '-JTAGConf', '-1,-1', '-speed', str(self.speed_khz), '-autoconnect', '1', '-ExitOnError', '1', '-NoGui', '1'] # Явная цепочка исключает интерактивный JTAGConf, преждевременно завершавший batch с кодом ноль.
return (['-USB', self.serial] if self.serial else []) + args # Серийный номер закрепляет конкретный программатор при нескольких подключениях.
def find_jlink(): # Обнаруживает существующий инструмент и ничего не скачивает в систему.
found = shutil.which('JLink.exe') or shutil.which('JLinkExe') # PATH может содержать также одноимённый сборщик модулей Java JDK.
candidates = ['C:/Keil_v5_41/ARM/Segger/JLink.exe', 'C:/Keil_v5/ARM/Segger/JLink.exe'] # Проверенные каталоги SEGGER в Keil имеют приоритет над неоднозначным PATH.
for variable in ('ProgramFiles', 'ProgramFiles(x86)'): # SEGGER может быть установлен в 32- или 64-разрядный каталог Windows.
if os.environ.get(variable): candidates.append(str(Path(os.environ[variable]) / 'SEGGER/JLink/JLink.exe')) # Не читает реестр и не требует прав администратора.
if found and not any((Path(found).parent / name).is_file() for name in ('java.exe', 'java', 'jmod.exe', 'jmod')): candidates.append(found) # Инструмент из bin Java JDK не является программатором SEGGER и не должен выбираться автоматически.
return next((str(Path(item)) for item in candidates if item and Path(item).is_file()), '') # Пустой результат предлагает явный выбор executable в форме.
def prepare_image(image, options): # Проверяет выбранный ранее FirmwareImage и сохраняет разреженную адресацию HEX.
options.validate() # Настройки проверяются до чтения внешних файлов и доступа к программатору.
path = Path(image.path) # Образ может прийти как из формы, так и из Публикатора.
if path.stat().st_size > 16 * 1024 * 1024: raise ValueError('Файл прошивки превышает 16 МиБ') # Ограничение покрывает 4 МиБ данных Intel HEX с текстовыми накладными расходами.
raw = path.read_bytes() # Читает единственный снимок исходника, который затем сравнивается с подтверждённым выбором.
if path.suffix.lower() == '.hex': # HEX уже содержит абсолютные адреса всех отдельных диапазонов Flash.
flat, base = contiguous_image(raw, options.flash_kib * 1024) # Канонический parser проверяет checksum, пересечения и полный адресный размах.
if flat != image.data or base != image.base_address: raise ValueError('Файл или адрес HEX изменён после выбора; выберите образ заново') # Не записывает новый файл под видом ранее выбранного образа и не смещает HEX.
segments = parse_intel_hex(raw) # Разрывы остаются отсутствующими записями, а не превращаются в байты FF для loadfile.
elif path.suffix.lower() == '.bin': # BIN требует явно указанного адреса, поскольку сам его не хранит.
if raw != image.data: raise ValueError('BIN изменён после выбора; выберите образ заново') # Не использует устаревший SHA-256 формы для другого содержимого.
segments = [HexSegment(image.base_address, raw)] # Адрес из формы относится к началу полезных байтов BIN.
else: raise ValueError('Для J-Link выберите Intel HEX или BIN') # OUT/ELF не обходят единый проверяемый план записи Flash.
if not segments or any(not part.data or part.start < FLASH_BASE or part.end >= FLASH_BASE + options.flash_kib * 1024 for part in segments): raise ValueError('Образ выходит за выбранный объём Flash STM32') # Не допускает запись RAM, option bytes или соседней памяти.
if options.uksvep: # Защита и manifest относятся только к конкретному порту UKSVEP.
if any(part.start < UKS_PARAMS + 2048 and part.end >= UKS_PARAMS for part in segments): raise ValueError('Образ затрагивает страницу параметров UKSVEP 0x0801F800') # Размер страницы F103RC равен 2048 байтам по профилю boot.
app = [part for part in segments if part.start < UKS_PARAMS and part.end >= UKS_APP] # Находит приложение без включения загрузчика и служебной страницы.
if app: # Запись одного app HEX также должна обновить CRC, иначе постоянный boot не разрешит запуск.
if len(app) != 1 or app[0].start != UKS_APP or app[0].end >= UKS_PARAMS: raise ValueError('Для UKSVEP нужен полный непрерывный образ приложения с 0x08008000') # Не выдаёт фрагментарный патч или смешанный BIN за полное приложение.
data = app[0].data # CRC считается по полезной длине приложения, без промежутков Flash.
if len(data) < 8: raise ValueError('В приложении отсутствует таблица векторов Cortex-M3') # Два первых слова содержат MSP и Reset_Handler.
sp, pc = struct.unpack_from('<II', data) # Векторы Cortex-M3 хранятся в little-endian.
if sp & 7 or not 0x20000000 < sp <= UKSVEP_F103RC_BOOT['stack_end'] or not pc & 1 or not UKS_APP <= (pc & ~1) < UKS_APP + len(data): raise ValueError('Неверные векторы приложения UKSVEP') # Требует SRAM 48 КиБ, выравнивание стека и Thumb-вход внутрь образа.
crc = zlib.crc32(data) & 0xFFFFFFFF # IEEE CRC32 совпадает с uks_boot_installed_valid() production-порта.
manifest = struct.pack('<6I', 0x554B5356, len(data), crc, ~len(data) & 0xFFFFFFFF, ~crc & 0xFFFFFFFF, UKSVEP_F103RC_BOOT['product']) # Magic и шесть слов заданы UKS_META_MAGIC/UKS_META_WORDS в boot_port.c.
existing = [part for part in segments if part.start < UKS_META + 2048 and part.end >= UKS_META] # Проверяет также явный manifest установочного HEX.
if existing and (len(existing) != 1 or existing[0].start != UKS_META or existing[0].data != manifest): raise ValueError('Manifest UKSVEP не соответствует приложению') # Не скрывает неверную служебную запись готового комплекта.
if not existing: segments.append(HexSegment(UKS_META, manifest)) # Для app-only JTAG формирует ту же запись активации, что UART/CAN boot.
return tuple(sorted(segments, key=lambda item: item.start)) # Неизменяемый отсортированный снимок используется для loadfile и последующего readback.
def encode_hex(segments): # Сохраняет только фактические диапазоны плана без заполнения защищённых разрывов.
lines, upper = [], None # Старшие шестнадцать бит записываются при каждом переходе между областями 64 КиБ.
def record(address, kind, data): # Стандартная строка Intel HEX содержит длину, адрес, тип, данные и дополнение суммы до нуля.
body = bytes((len(data), address >> 8, address & 255, kind)) + data # Адрес внутри записи ограничен текущим окном 64 КиБ.
return ':' + (body + bytes((-sum(body) & 255,))).hex().upper() # Проверяемая checksum делает снимок пригодным для независимого J-Link parser.
for part in segments: # Каждый диапазон начинает адресованные данные без добавления промежутков.
offset = 0 # Положение выражено в байтах текущего сегмента.
while offset < len(part.data): # Не создаёт пустых записей в конце последнего блока.
address = part.start + offset # Вычисляет абсолютный адрес следующей порции.
if address >> 16 != upper: # Type 04 обновляет старшую половину линейного адреса.
upper = address >> 16 # Последующие data-записи используют именно это окно.
lines.append(record(0, 4, upper.to_bytes(2, 'big'))) # Порядок байтов type 04 задан Intel HEX и не зависит от Cortex-M.
count = min(16, len(part.data) - offset, 0x10000 - (address & 0xFFFF)) # Строка не пересекает границу окна даже при невыровненном начале.
lines.append(record(address & 0xFFFF, 0, part.data[offset:offset + count])) # Добавляет только реальные байты исходного диапазона.
offset += count # Следующая строка продолжает непрерывные данные данного сегмента.
return '\n'.join(lines + [record(0, 1, b'')]) + '\n' # EOF завершает UTF-8/ASCII-снимок с обычными переводами строк.
def quote_path(path): # Экранирование J-Link command file отличается от аргументов системного процесса.
value = Path(path).resolve().as_posix() # Абсолютные пути исключают зависимость backup от рабочего каталога.
if any(char in value for char in ('"', '\r', '\n')): raise ValueError('Путь J-Link не должен содержать кавычки или переносы строк') # Не позволяет имени каталога стать дополнительной командой Commander.
return '"' + value + '"' # Пробелы и кириллица остаются частью одного имени файла.
def programming_script(folder, options): # Первый этап сохраняет backup и readback, оставляя ядро остановленным до независимой проверки.
folder = Path(folder) # Все артефакты принадлежат отдельному каталогу одной операции.
length = hex(options.flash_kib * 1024) # SaveBin принимает длину полного Flash в байтах.
return '\n'.join(['ExitOnError 1', 'r', 'h', f'savebin {quote_path(folder / "before.bin")} 0x08000000 {length}', f'loadfile {quote_path(folder / "image.hex")}', f'savebin {quote_path(folder / "after.bin")} 0x08000000 {length}', 'q', '']) # Полного erase нет; loadfile программирует разреженный проверенный HEX с собственной Verify.
def verify_readback(segments, options, before, after): # Проверяет аппаратные снимки независимо от кода возврата Commander.
if len(before) != options.flash_kib * 1024 or len(after) != len(before): raise ValueError('J-Link не сохранил полный backup/readback Flash; запуск отменён') # Нулевой код интерактивно прерванного Commander не считается успехом.
for part in segments: # Каждая реально записываемая область должна совпасть побайтно.
offset = part.start - FLASH_BASE # Смещение внутри полного аппаратного снимка.
if after[offset:offset + len(part.data)] != part.data: raise ValueError(f'Ошибка проверки Flash по адресу 0x{part.start:08X}; запуск отменён') # Не запускает MCU после неполного программирования.
if options.uksvep: # Проверяет сохранность старых настроек именно там, где их хранит UKSVEP.
offset = UKS_PARAMS - FLASH_BASE # Страница параметров находится между boot/app и manifest.
if before[offset:offset + 2048] != after[offset:offset + 2048]: raise ValueError('Изменилась страница параметров UKSVEP; запуск отменён') # Проверка касается реальной Flash, а не только отсутствия области в HEX.
def start_script(options): # Второй этап выполняется только после успешной проверки readback на хосте.
commands = ['ExitOnError 1', 'r'] # Сброс снимает состояние RAM-алгоритма Flash, оставшееся после loadfile.
if options.uksvep: commands += ['w4 0x4002101C 0x18000000', 'w4 0x40007000 0x00000100', 'w4 0x40006C24 0x00000000', 'w4 0x40006C28 0x00000000'] # F103RC: включение PWR/BKP, DBP bit8 и очистка DR9/DR10 убирают старый одноразовый запрос boot без стирания резервного домена.
return '\n'.join(commands + ['g', 'q', '']) # Возобновляет выполнение после reset и освобождает программатор без повторной записи Flash.
+4 -3
View File
@@ -13,13 +13,14 @@ def decode_gas167(frame, node):
result.protocol = "УМП %d / ProtoCAN GAS 167/v1" % node
if not frame.ide or frame.rtr or frame.is_error:
return result
if result.id.msg_type not in (pc.MsgType.GENERAL_ADDRESS_SPACE,
pc.MsgType.MODBUS_HOLDING):
if result.id.msg_type not in (pc.MsgType.GENERAL_ADDRESS_SPACE, pc.MsgType.MODBUS): # Текущий ProtoCAN использует единый тип Modbus 0x5; отдельного MODBUS_HOLDING больше нет.
return result
if result.id.msg_type == pc.MsgType.GENERAL_ADDRESS_SPACE:
start = result.id.body
else:
start, _count = pc.split_modbus(result.id.body)
start, _count, function = pc.split_modbus(result.id.body) # Современный Body возвращает также функцию, определяющую банк регистров.
if function != pc.ModbusFunction.HOLDING: # Ручная карта GAS 167/v1 относится к прежнему банку Holding, а не к дискретам или входным регистрам Modbus.
return result # Остальные банки сохраняют штатную расшифровку ProtoCAN без ошибочной подстановки адресов карты УМП.
words = result.registers
if words is None:
return result
+51
View File
@@ -0,0 +1,51 @@
"""Эталонные обмены SCI ROM и TI Flash Kernel A без настоящей платы.""" # Проверки не открывают COM и не вызывают внешнюю программу C2Prog.
import struct # Формирует независимые тестовые SCI8-векторы с явно заданными словами и адресами.
import pytest # Проверяет отказ при повреждениях и граничные размеры порций ядра TI.
from protocan.c2000_sci import Session, decode, validate, ulitco_enter # Испытывает настоящий ctypes-порт общего C99-ядра.
def sci8(address, words): # Готовит минимальную адресную таблицу для эталонных данных теста.
pointer = struct.pack("<HH", address >> 16, address & 65535) # SCI8 хранит старшее слово адреса перед младшим.
return b"\xaa\x08" + bytes(16) + pointer + struct.pack("<H", len(words)) + pointer + struct.pack("<" + "H" * len(words), *words) + bytes(2) # Вектор содержит ключ, резерв, entry, одну секцию и терминатор.
@pytest.mark.parametrize("words", [1, 1024, 1025, 2048, 2050]) # Границы проверяют обычную порцию, остаток и дополнительный нулевой ACK официального TI kernel.
def test_complete_echo_and_checksum_transcript(words): # Фактические ответы подтверждают порядок и неизменность всего приложения.
kernel, image = sci8(0x9000, [0x1234, 0x5678]), sci8(0x328000, [0xA55A] * words) # RAM-ядро и Flash занимают разные разрешённые области.
session, received, pauses, zero_acks = Session(kernel, image), bytearray(), [], 0 # Наблюдения не используют внутреннюю структуру C-контекста.
for _ in range(len(kernel) + words + 100): # Конечный предел выявляет зацикливание автомата.
state, tx, expected, delay = session.next() # Проверяет настоящий сформированный C-пакет.
if state == 0: break # Успех допустим лишь после отправки нулевого терминатора приложения.
if state == 2: pauses.append(delay); continue # Внешний транспорт выполняет только заданную алгоритмом задержку.
if expected == 1: session.accept(tx) # ROM и autobaud должны вернуть точное эхо отправленного октета.
else: # Kernel A подтверждает сумму октетов после erase/program, без побайтового эха приложения.
received.extend(tx) # Снимок подтверждает отсутствие пропущенных, повторённых либо изменённых октетов SCI8.
if expected: session.accept(struct.pack("<H", sum(tx) & 65535)) # Независимый эталон суммы следует опубликованному исходнику TI SendCheckSum.
if not tx: zero_acks += 1 # Пустой остаток TI подтверждается отдельным нулевым словом.
else: pytest.fail("Автомат SCI не завершился") # Отсутствие терминального перехода считается ошибкой протокола.
assert bytes(received) == image # MCU получил ровно исходный Flash SCI8 без переименования или подмены содержимого.
assert pauses == [3000, 3000] and session.progress == 100 # Порядок ожиданий совпадает с TI f05_DownloadImage.
assert zero_acks == int(words > 1024 and words % 1024 == 0) # Обрабатывает особенность исходного TI Kernel A на кратной порции.
def test_corrupt_echo_and_checksum_stop_transfer(): # Повреждённое подтверждение не должно разрешать следующий пакет.
kernel, image = sci8(0x9000, [1]), sci8(0x328000, [2]) # Минимальные векторы отделяют проверку ответа от больших файлов.
session = Session(kernel, image); session.next() # Первый пакет требует эхо ASCII A.
with pytest.raises(ValueError, match="не совпали"): session.accept(b"B") # Чужой байт autobaud не принимается как связь с ROM.
session = Session(kernel, image) # Независимый сеанс проверяет повреждение двухоктетной суммы Flash.
while True: # Доходит до первой операции erase/header без использования внутренних фаз автомата.
state, tx, expected, _ = session.next() # Паузы не требуют аппаратного ответа.
if state == 2: continue # Номер стадии остаётся внутри C-ядра.
if expected == 1: session.accept(tx) # Корректный ROM позволяет загрузить RAM-ядро.
else: break # Первый двухоктетный ответ должен подтвердить заголовок приложения.
with pytest.raises(ValueError, match="не совпали"): session.accept(b"\xff\xff") # Неверная сумма не разрешает программирование следующей секции.
def test_ascii_decode_memory_ranges_and_monitor_command(): # Проверяет переносимость форматов и аппаратные границы до открытия порта.
image = sci8(0x328000, [0x1234]) # Обычный Flash-вектор не пересекает CSM.
ascii_image = b"\x02\n" + " ".join(f"{value:02X}" for value in image).encode("ascii") + b"\n\x03\n" # Формат соответствует выводу TI hex2000 -boot -sci8 -a.
assert decode(ascii_image) == image and validate(image) == (0x328000, 4) # Сектор C имеет маску 0x0004 в TI Flash API.
with pytest.raises(ValueError): decode(ascii_image[:-2]) # Усечённый ETX не должен оставить частично декодированное ядро.
with pytest.raises(ValueError): validate(sci8(0x33FFF8, [0x1234])) # Область CSM-паролей запрещена независимо от имени файла.
with pytest.raises(ValueError): validate(sci8(0x8000, [0x1234])) # RAM-образ не может стать Flash-приложением из-за выбранного loader.
assert ulitco_enter() == bytes([247, 213, 255, 93, 0, 0, 1, 0, 93, 1]) # Эталон совпадает с десятиоктетным Monitor-запросом FLASH_NUM=93 текущей прошивки UlitCo.
+99
View File
@@ -0,0 +1,99 @@
"""Проверки адресованной прошивки и защиты данных без подключения реального MCU.""" # Подмена касается только файлов снимков, production-план остаётся настоящим.
from dataclasses import replace # Позволяет воспроизводить неверные настройки и адрес BIN независимо от формы.
from pathlib import Path # Тесты создают все входы только во временном каталоге pytest.
import struct # Создаёт корректные Cortex-M3-векторы и проверяет настоящий формат manifest UKSVEP.
import zlib # Независимо вычисляет ожидаемый CRC32 приложения.
import pytest # Параметры покрывают ошибки до первой аппаратной записи.
from firmware_image.core import HexSegment, parse_intel_hex # Канонический parser проверяет сформированный снимок для Commander.
from set_devices.firmware import FirmwareImage # Испытывает тот же тип образа, который передаёт SETGUI.
from set_devices.stm32_jtag import JLinkOptions, FLASH_BASE, UKS_APP, UKS_PARAMS, UKS_META, prepare_image, encode_hex, programming_script, start_script, verify_readback # Проверяет публичную переносимую реализацию templates.
from set_devices import stm32_jtag as module # Позволяет изолировать поиск установленного программатора от настоящего PATH Windows.
def app_image(tmp_path, extra=()): # Создаёт небольшой корректный app HEX и необязательные отдельные диапазоны.
data = struct.pack('<II', 0x2000C000, UKS_APP + 9) + bytes(range(64)) # MSP соответствует 48-КиБ SRAM; Thumb Reset_Handler находится внутри payload.
path = tmp_path / 'app.hex' # Исходный файл не содержит служебного manifest, как обычный postbuild приложения.
path.write_text(encode_hex([HexSegment(UKS_APP, data), *extra]), encoding='utf-8') # Реальный Intel HEX проходит штатную загрузку FirmwareImage.
return FirmwareImage.load(path) # Позволяет проверять также сохранность выбора и отказ при изменении файла.
def test_app_only_gets_production_manifest_and_sparse_snapshot(tmp_path): # JTAG app-only обязан остаться запускаемым постоянным загрузчиком.
image, options = app_image(tmp_path), JLinkOptions() # Использует проверенный профиль UKSVEP STM32F103RC.
plan = prepare_image(image, options) # Production-план автоматически добавляет запись активации приложения.
assert [(part.start, len(part.data)) for part in plan] == [(UKS_APP, 72), (UKS_META, 24)] # Параметры и разрыв не превращаются в данные для записи.
magic, size, crc, nsize, ncrc, product = struct.unpack('<6I', plan[-1].data) # Формат должен читаться настоящим boot_port.c без адаптации.
assert (magic, size, crc, nsize, ncrc, product) == (0x554B5356, 72, zlib.crc32(image.data), ~72 & 0xFFFFFFFF, ~zlib.crc32(image.data) & 0xFFFFFFFF, 0x5606) # Проверяет взаимные инверсии и аппаратный Product UKSVEP.
assert tuple(parse_intel_hex(encode_hex(plan))) == plan # Снимок Commander сохраняет точные адреса и байты обоих диапазонов.
assert not any(part.start <= UKS_PARAMS <= part.end for part in plan) # Страница настроек отсутствует в адресованных data-записях.
def test_factory_image_with_boot_and_manifest_is_not_flattened(tmp_path): # Полный install HEX не должен стереть промежуточную страницу параметров.
image = app_image(tmp_path, [HexSegment(FLASH_BASE, b'boot' * 64)]) # Адрес загрузчика отделён от приложения разрывом.
plan = prepare_image(image, JLinkOptions()) # Сначала получает правильный manifest для готового установочного комплекта.
image.path.write_text(encode_hex(plan), encoding='utf-8') # Моделирует опубликованный boot+app+manifest HEX.
assert prepare_image(FirmwareImage.load(image.path), JLinkOptions()) == plan # Повторная подготовка не добавляет FF и не меняет готовый manifest.
@pytest.mark.parametrize('address', [UKS_PARAMS - 1, UKS_PARAMS, UKS_PARAMS + 2047]) # Касание любого края защищённой страницы запрещено.
def test_parameter_page_overlap_is_rejected(tmp_path, address): # Ошибка выявляется до обращения к J-Link.
image = app_image(tmp_path, [HexSegment(address, b'xx')]) # Два байта проверяют также вход в страницу с предыдущего адреса.
with pytest.raises(ValueError, match='страницу параметров'): prepare_image(image, JLinkOptions()) # Полный дамп нельзя случайно записать вместо установочного разреженного HEX.
def test_stale_image_and_relocated_hex_are_rejected(tmp_path): # Выбор и подтверждение должны относиться к тем же байтам и адресам.
image = app_image(tmp_path) # Сохраняет исходный снимок формы до вмешательства в файл.
with pytest.raises(ValueError, match='адрес HEX'): prepare_image(replace(image, base_address=UKS_APP + 4), JLinkOptions()) # Явный адрес HEX нельзя переопределять как адрес BIN.
image.path.write_text(encode_hex([HexSegment(UKS_APP, image.data + b'changed')]), encoding='utf-8') # Моделирует пересборку файла после выбора пользователем.
with pytest.raises(ValueError, match='изменён'): prepare_image(image, JLinkOptions()) # До повторного выбора аппаратное программирование запрещено.
def test_bin_needs_address_and_accepts_another_stm32(tmp_path): # Общий режим не применяет специальные адреса и manifest UKSVEP.
path = tmp_path / 'raw.bin' # BIN не содержит собственной карты памяти.
path.write_bytes(bytes(range(64))) # Для обычного MCU не навязывается структура приложения UKSVEP.
image, options = FirmwareImage.load(path), JLinkOptions(device='STM32G474VE', flash_kib=512, interface='SWD', uksvep=False) # Использует другую физическую карту STM32.
with pytest.raises(ValueError, match='объём Flash'): prepare_image(image, options) # Нулевой адрес BIN не означает RAM или неявное начало Flash.
assert prepare_image(replace(image, base_address=FLASH_BASE), options) == (HexSegment(FLASH_BASE, image.data),) # Явный корректный адрес создаёт ровно один диапазон без постороннего manifest.
@pytest.mark.parametrize('changes', [{'device': 'STM32F103RC\ng'}, {'serial': '123 -erase'}, {'interface': 'UART'}, {'speed_khz': 0}, {'flash_kib': 0}, {'device': 'STM32F407VE'}]) # Проверяет интерфейс, размеры, профиль и внедрение дополнительных команд.
def test_invalid_target_is_rejected(changes): # Поля будущего command file валидируются единым контрактом.
with pytest.raises(ValueError): replace(JLinkOptions(), **changes).arguments() # Ошибка не требует создания процесса и не открывает программатор.
def test_readback_rejects_partial_write_and_lost_parameters(tmp_path): # Ложный нулевой exit code не должен привести к запуску испорченного MCU.
options = JLinkOptions() # Сохранность параметров проверяется для конкретного UKSVEP-профиля.
plan = prepare_image(app_image(tmp_path), options) # Использует реальный production-план с manifest.
before = bytes((index * 7) & 255 for index in range(256 * 1024)) # Полный backup содержит не-FF настройки, чтобы их потеря была заметной.
after = bytearray(before) # Моделирует неизменённые области между реально записанными диапазонами.
for part in plan: after[part.start - FLASH_BASE:part.start - FLASH_BASE + len(part.data)] = part.data # Применяет только адресованные данные плана.
verify_readback(plan, options, before, after) # Полная корректная запись разрешает отдельный этап reset/run.
after[UKS_PARAMS - FLASH_BASE] ^= 1 # Воспроизводит потерю одного бита существующих параметров.
with pytest.raises(ValueError, match='параметров'): verify_readback(plan, options, before, after) # Сохранность protected page проверяется по аппаратным снимкам.
after[UKS_PARAMS - FLASH_BASE] ^= 1 # Восстанавливает параметры для отдельной проверки ошибки программирования приложения.
after[UKS_APP - FLASH_BASE + 8] ^= 1 # Повреждает один реально записываемый байт приложения.
with pytest.raises(ValueError, match='Ошибка проверки'): verify_readback(plan, options, before, after) # Запуск запрещён даже при совпавшей длине обоих SaveBin.
with pytest.raises(ValueError, match='полный backup'): verify_readback(plan, options, before, b'') # Преждевременный выход Commander с кодом ноль не считается успехом.
def test_scripts_separate_programming_from_reset_run(tmp_path): # Архитектура не должна запускать ядро до проверки на хосте.
options = JLinkOptions(serial='603000869') # Явный серийный номер должен сохраняться отдельным аргументом.
program, run = programming_script(tmp_path / 'folder with spaces', options), start_script(options) # Пути с пробелами должны быть корректно заключены в кавычки.
assert program.index('before.bin') < program.index('loadfile') < program.index('after.bin') # Backup выполняется до первой записи, readback — после неё.
assert '\ng\n' not in program and '\ng\n' in run and 'loadfile' not in run # Отдельный проверенный этап содержит только reset/служебные регистры/run.
assert 'erase' not in program.lower() # Не вводит массовое стирание через новую общую реализацию.
assert options.arguments()[:2] == ['-USB', '603000869'] # Выбранный J-Link не заменяется другим подключённым адаптером.
assert '-JTAGConf' in options.arguments() and '-1,-1' in options.arguments() # Batch не должен остановиться на интерактивном запросе цепочки JTAG.
def test_hex_encoder_handles_unaligned_64k_boundary(): # Граничный адрес внутри сегмента не должен переполнить 16-битное поле записи.
segments = (HexSegment(0x0800FFF9, bytes(range(40))),) # Невыровненный старт пересекает окно 64 КиБ внутри первой порции.
assert tuple(parse_intel_hex(encode_hex(segments))) == segments # Независимый C-parser подтверждает корректную смену Extended Linear Address.
def test_autodetection_does_not_select_java_jlink(monkeypatch): # Windows PATH может раньше SEGGER содержать одноимённый инструмент JDK.
java = 'C:/jdk/bin/JLink.exe' # Имя совпадает без учёта регистра с программатором SEGGER.
monkeypatch.setattr(module.shutil, 'which', lambda name: java) # Установленный в системе J-Link не участвует в этой проверке.
monkeypatch.setattr(Path, 'is_file', lambda path: path.as_posix() in (java, 'C:/jdk/bin/java.exe')) # Моделирует только bin JDK и отсутствие Keil/SEGGER.
assert module.find_jlink() == '' # Общая форма должна запросить настоящий executable вместо запуска Java.
monkeypatch.setattr(Path, 'is_file', lambda path: path.as_posix() in (java, 'C:/jdk/bin/java.exe', 'C:/Keil_v5_41/ARM/Segger/JLink.exe')) # Добавляет именно установленный SEGGER, не меняя неоднозначный PATH.
assert module.find_jlink().replace('\\', '/') == 'C:/Keil_v5_41/ARM/Segger/JLink.exe' # Проверенный каталог Keil получает приоритет над JDK.

Some files were not shown because too many files have changed in this diff Show More