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

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

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

Проверено: шесть испытаний модели, включая обрывы записи и восстановление.
2026-11-13 15:07:55 +03:00
Andrey 7b1a16d6e4 Исправить сборку CCS и Keil на Python 3.7
Убрать несовместимый параметр missing_ok при очистке временных файлов.
Обеспечить повторное копирование архивов без параметра dirs_exist_ok.
Добавить проверки генерации заголовков и экспорта с API Python 3.7.
2026-11-10 15:13:35 +03:00
52 changed files with 1536 additions and 965 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/
-15
View File
@@ -1,15 +0,0 @@
cmake_minimum_required(VERSION 3.16) # Эта версия поддерживает используемые target_compile_features и CTest без платформенных расширений.
project(can485_monitor C) # Переносимое ядро собирается независимо от Linux-порта и приложения SETGUI.
add_library(can485_monitor STATIC src/can485_monitor.c) # Все платформы связываются с одним обработчиком команд и wire-форматов.
target_include_directories(can485_monitor PUBLIC include) # Потребитель получает только публичный C-контракт библиотеки.
target_compile_features(can485_monitor PUBLIC c_std_99) # Ядро не требует C11 и платформенных расширений языка.
include(CTest) # Стандартный BUILD_TESTING управляет автономной проверкой библиотеки.
if(BUILD_TESTING) # Host-проверка не требует Linux, Qt и физической CAN-шины.
add_executable(test_can485_monitor tests/test_can485_monitor.c) # Использует независимые wire-векторы и фикстуру аппаратных отказов.
target_link_libraries(test_can485_monitor PRIVATE can485_monitor) # Проверяет настоящую библиотеку, не тестовую копию алгоритма.
add_test(NAME can485_monitor_contract COMMAND test_can485_monitor) # Регистрирует единый поведенческий контракт для всех аппаратных портов.
endif() # Завершает необязательную сборку тестов.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux") # Linux-заголовки не попадают в сборки ESP32, Windows и других MCU.
add_executable(zynq-can485d ports/zynq-linux/main.c) # SocketCAN, USB и Ethernet реализуются только аппаратным портом.
target_link_libraries(zynq-can485d PRIVATE can485_monitor) # Linux использует ровно то же C99-ядро, что host-проверка.
endif() # Завершает платформенную часть библиотеки.
-82
View File
@@ -1,82 +0,0 @@
# CAN485 monitor
Общее C99-ядро команд CAN485 DevBoard/SETGUI и Linux-порт для PS CAN Zynq.
Протоколы CAN (SETCAN, ProtoCAN, УМП, Balsam, DEIF и остальные) разбираются
существующими библиотеками `templates/python`, без копий в проекте платы.
Слои: SETGUI BoardTab → `qt_ports/can485_port.py` → Ethernet TCP или USB tty →
`ports/zynq-linux/main.c` → `can485_monitor.c` → SocketCAN / RS485 / каталог SD.
В ядре нет Linux, Qt, HAL, выделения памяти и глобального контекста.
| Файл | Назначение и зависимости |
|---|---|
| `include/can485_monitor.h` | Контракт C99, `stdint.h`, `stddef.h` |
| `src/can485_monitor.c` | Проверка команд, фильтр, маршруты, AA55/CRC8, строки CAN |
| `ports/zynq-linux/main.c` | SocketCAN, rtnetlink, TCP, USB tty, UART RS485, SD, POSIX/Linux |
| `tests/test_can485_monitor.c` | Эталонный пакет, ошибки/границы, маршрутизация, отказы оборудования |
Порт предоставляет четыре callback: `emit(context,line)`,
`configure(context,kbps,mode,running)`, `send(context,frame,source)` и
`control(context,command)`. `source=0` означает CAN, `source=1` — RS485.
`configure` и `send` возвращают 1 только после принятия операции драйвером.
Аппаратный ACK этим результатом не подтверждается. Контекст и буферы принадлежат приложению.
```c
#include "can485_monitor.h" /* Подключает переносимое ядро библиотеки. */
void example(cm_port port) { /* Аппаратные callback передаются вызывающей платформой. */
cm_monitor state; /* Контекст не разделяется с другим экземпляром моста. */
cm_init(&state, port); /* CAN остановлен, оба направления маршрута выключены. */
cm_command(&state, "V"); /* Версия и статус не меняют оборудование. */
cm_command(&state, "S125"); /* Запрашивает CAN 125 кбит/с через аппаратный callback. */
cm_command(&state, "M1"); /* Listen-only запрещает ручной TX и RS485→CAN. */
cm_frame frame = {0x123, 2, 0, {0xAB, 0xCD}}; /* STD ID и два исходных байта. */
cm_receive(&state, &frame, 0, 12345); /* Приём CAN в момент 12,345 с монотонного времени. */
cm_status(&state, 12345, "sd=idle"); /* Статус содержит только известные платформе поля. */
} /* Реальная плата передаёт только фактически принятые кадры в cm_receive. */
```
Поддержаны команды `V ? H S M O C F Q R B T X Z L D G I` и Linux-расширение
`Ncan0` / `Ncan1`. Команды строго проверяются целиком. RS485-пакет:
`AA 55 FLAGS ID[4]be DLC DATA[8] CRC8(FLAGS..DATA, poly=0x07, init=0)`.
CAN STD/EXT/RTR сохраняются. DATA TX через `T` повторяет контракт существующего ESP GUI.
Linux-порт запускается с CAN остановленным, маршрутами и записью SD выключенными.
`S`/`M`/`O` запускают CAN; `C` останавливает интерфейс. Настройка через rtnetlink
требует root или `CAP_NET_ADMIN`; RAW CAN — соответствующих прав Linux.
M2 требует реальной поддержки `CAN_CTRLMODE_PRESUME_ACK`; ошибка драйвера
не маскируется под успешный no-ack. Фильтр программный и не отключает аппаратный ACK.
TCP-порт по умолчанию **37579**, один управляющий клиент. Подключённый USB
может получать поток, но команды второго транспорта возвращают `#ERR busy`.
Второй TCP-клиент не вытесняет первого. При обрыве владельца маршруты выключаются,
его SD-download отменяется. Медленный клиент отключается при заполнении очереди 64 КиБ.
Служба не включает периодический CAN TX: этот таймер принадлежит SETGUI и
останавливается при отключении. Смена `N` закрывает только собственный CAN socket.
`--usb /dev/ttyGS0` использует USB OTG CDC ACM, если образ Linux предоставляет
gadget serial. Также допустим выделенный UART через USB-UART, свободный от
console/getty. Разъём USB HOST и USB-JTAG сами по себе CDC не создают.
USB и RS485 должны использовать разные tty. RTS/CTS выключены. RS485 требует
физического трансивера с аппаратным управлением направлением либо предварительной
настройки драйвера 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 байт,
с проверяемыми offset/size/CRC32. Путь ограничен одним каталогом, `..` и symlink
запрещены. Запись SD выключается перед чтением. `vin`, TEC и REC не выдумываются,
если платформа не получила реальные значения.
Используется: Zynq RK7020 (`demo/can485`) и CAN-панель SETGUI (`pc/zynq_can_panel.py`).
Для единого физического источника задавайте одинаковый **TEMPLATES_ROOT** при
запуске SETGUI и сборке ARM (`build.ps1 -Templates <тот же каталог>`).
После изменения Python перезапустите приложение; изменение C требует пересборки
службы, изменение встроенных библиотек EXE — пересборки EXE. Подмодули разных
checkout обновляются явно: Git не обновляет уже собранную прошивку автоматически.
-24
View File
@@ -1,24 +0,0 @@
#ifndef CAN485_MONITOR_H /* Защищает публичный контракт монитора от повторного включения. */
#define CAN485_MONITOR_H /* Одинаковое ядро обслуживает команды DevBoard на разных платформах. */
#include <stdint.h> /* Фиксирует размеры CAN ID, счётчиков и байтов независимо от процессора. */
#include <stddef.h> /* Размеры выходных строк выражены штатным типом size_t. */
typedef struct { uint32_t id; uint8_t dlc, flags, data[8]; } cm_frame; /* flags: бит 0 EXT, бит 1 RTR; Classic CAN содержит до восьми байтов. */
typedef struct { /* Платформа предоставляет ввод-вывод, не копируя разбор команд. */
void (*emit)(void *, const char *); /* Принимает одну законченную строку с LF для Ethernet, USB и журнала. */
int (*configure)(void *, unsigned, unsigned, int); /* Применяет битрейт в кбит/с, режим M0/M1/M2 и состояние драйвера; ноль означает ошибку. */
int (*send)(void *, const cm_frame *, int); /* Передаёт кадр в CAN при source=0 либо в RS485 при source=1; успех означает принятие локальным драйвером. */
int (*control)(void *, const char *); /* Обслуживает аппаратную скорость B и файловые L/D/G/I; ответы формирует сам порт. */
void *context; /* Контекст принадлежит вызывающему коду и позволяет иметь несколько независимых мостов. */
} cm_port; /* В ядре нет HAL, Linux, Qt, динамической памяти и глобального состояния. */
typedef struct { /* Состояние протокола отделено от сетевых клиентов и файловых дескрипторов. */
cm_port port; unsigned bitrate, mode, route, baud, running; /* Скорости заданы в кбит/с для CAN и бод для RS485; route использует два младших бита направлений. */
uint32_t filter_id, filter_mask, rx, tx, rsrx, rstx, buserr, dropcan, droprs, rscrc; /* Счётчики относятся к фактическим операциям порта; фильтр сравнивает только 29 бит ID. */
uint64_t now; /* Последнее подтверждённое монотонное время позволяет ответу V сохранять реальный uptime между периодическими статусами. */
} cm_monitor; /* Всё изменяемое состояние хранится в экземпляре вызывающего приложения. */
void cm_init(cm_monitor *, cm_port); /* Инициализирует остановленный CAN 125 кбит/с, normal, маршруты выключены и RS485 512000 бод. */
void cm_command(cm_monitor *, const char *); /* Проверяет полную команду без CR/LF и применяет её только после успешной валидации. */
void cm_receive(cm_monitor *, const cm_frame *, int, uint64_t); /* Принимает кадр CAN=0 либо RS485=1 и монотонное время в миллисекундах. */
void cm_status(cm_monitor *, uint64_t, const char *); /* Печатает совместимый #STAT; дополнительная строка содержит подтверждённые поля SD/Linux. */
int cm_packet_decode(const uint8_t *, cm_frame *); /* Проверяет все 17 байт AA55/CRC8, формат ID и DLC до маршрутизации RS485. */
void cm_packet_encode(const cm_frame *, uint8_t *); /* Формирует RS485 AA55, FLAGS, ID big-endian, DLC, восемь байтов и CRC8/0x07. */
#endif /* Завершает независимый публичный контракт CAN485 monitor. */
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+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
@@ -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.
+26 -2
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,7 +79,11 @@ 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)
# A clean application built with a modified templates checkout is dirty too.
known = source["commit"] is not None and dependency["commit"] is not None
@@ -90,6 +100,7 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
"version": version, "build_id": build_id, "built_at_utc": stamp.isoformat(),
"source": source, "templates": dependency,
"identity_contract": 1, "identity_register_count": 12}
if "flashing" in settings: metadata["flashing"] = flashing # Переносит все CAN/RS-профили в prebuild, manifest, архив и комплект Публикатора без изменения бинарного протокола паспорта.
constants = dict(zip(("FIRMWARE_VERSION_MAJOR", "FIRMWARE_VERSION_MINOR", "FIRMWARE_VERSION_PATCH"), version))
constants.update(dict(zip(("FI_YEAR", "FI_MONTH", "FI_DAY", "FI_HOUR", "FI_MINUTE", "FI_SECOND"),
(stamp.year, stamp.month, stamp.day, stamp.hour, stamp.minute, stamp.second))))
@@ -152,6 +163,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 +185,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
+4 -4
View File
@@ -10,6 +10,7 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
# SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
set(SETPROTOCOL_V2_SOURCES
src/c2000_sci.c # Собирает общий автомат SCI Boot и TI Flash Kernel A для F28335 во всех портах.
src/firmware_image.c
src/altera_logic.c
src/altera_stream.c
@@ -46,7 +47,6 @@ set(SETPROTOCOL_LEGACY_SOURCES
)
set(SETPROTOCOL_SOURCES
src/set_modbus_gateway.c # Единый кодек USB RTU, Ethernet TCP и таблицы маршрутов используется платой и Qt-панелью.
src/deif_xdi.c # Общее C99-ядро DEIF для всех платформ.
src/set_invertor_gas.c # Подключает две проекции одной карты STM Invertor: GAS 0x3 и масочный GAS SLOT 0x4.
src/set_deif_gauge.c
@@ -100,10 +100,10 @@ endif()
# JNI/CMSIS-порты подключаются целевым проектом и в host CMake не входят.
option(SETP_BUILD_TESTS "Build host tests" ON)
if(SETP_BUILD_TESTS)
add_executable(test_modbus_gateway tests/test_modbus_gateway.c) # Эталонные ADU защищают маршрутизацию, CRC и настройки от расхождений между портами.
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) # Host-проверка использует ту же реализацию, что ctypes и будущие порты.
add_test(NAME c2000_sci COMMAND test_c2000_sci) # Делает контракт штатного загрузчика F28335 частью обычного CTest.
add_executable(test_deif_xdi tests/test_deif_xdi.c) # Прямые проверки C-контракта DEIF.
target_link_libraries(test_deif_xdi PRIVATE setprotocol_static) # Используется единственное общее ядро.
add_test(NAME deif_xdi COMMAND test_deif_xdi "${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/deif-xdi-v1.tsv") # Векторы общие с JNI и Python.
-6
View File
@@ -131,9 +131,3 @@ FFI-клиенты подключают `setprotocol_abi.h`. Имена функ
## GAS: регистры 0x3 и слоты 0x4
[Адресация GAS](docs/GAS_SLOT.md): `0x3` — отдельные 16-битные регистры `0…65535`, запись 1…4 слов. `0x4` — слот с Word0-маской и тремя значениями; PERIPH сохраняет 200 слов в 67 слотах `0xMM00…0xMM42`. Формат определяется MsgType, без настройки профиля.
## Modbus: шлюз USB / TCP
`set_modbus_gateway.h` содержит общий C99-кодек RTU ↔ TCP, проверку ответов
и формат таблицы маршрутов без зависимости от Linux и Qt. Контракт порта
и устройство слоёв описаны в [MODBUS_GATEWAY.md](../../doc/MODBUS_GATEWAY.md).
+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. */
@@ -1,31 +0,0 @@
#ifndef SET_MODBUS_GATEWAY_H /* Защищает единый контракт шлюза от повторного включения. */
#define SET_MODBUS_GATEWAY_H /* Кодек используется платой, Qt-портом и независимыми тестами. */
#include "pcan_abi.h" /* Предоставляет экспорт C ABI, size_t и целочисленные типы фиксированной ширины. */
#ifdef __cplusplus /* Позволяет подключать ядро из C++ без изменения имён экспортируемых функций. */
extern "C" { /* Все точки входа сохраняют C ABI для ctypes и прошивок. */
#endif /* Завершает условие C++-линковки. */
#define SMG_ADU 260u /* Максимум TCP: MBAP 7 байт и PDU 253 байта по спецификации Modbus. */
#define SMG_ROUTES 20u /* Двадцать записей по 10 байт помещаются вместе с настройками в один PDU. */
#define SMG_CONTROL_UNIT 248u /* Зарезервированный адрес управления исключает конфликт с RTU slave 1…247. */
typedef struct { uint16_t transaction, length; uint8_t unit, pdu[253]; } smg_request; /* Поля хранятся в native-порядке; структура никогда не передаётся по проводу. */
typedef struct { uint8_t source, target, transport, interface_id; uint32_t ipv4; uint16_t port; } smg_route; /* transport: 0 RS485, 1 TCP; interface_id: 0 маршрут Linux, 1 eth0, 2 eth1. */
typedef struct { uint32_t baud; uint16_t timeout_ms; uint8_t parity, stop_bits, default_serial, count; smg_route routes[SMG_ROUTES]; } smg_config; /* parity: 0 N, 1 E, 2 O; адреса IPv4 записаны как числовое значение big-endian. */
PCAN_ABI_API int smg_tcp_size(const uint8_t *, size_t); /* Возвращает полный размер ADU, 0 до получения MBAP либо -1 при неверном заголовке. */
PCAN_ABI_API int smg_rtu_size(const uint8_t *, size_t); /* Вычисляет размер запроса поддерживаемой функции без зависимости от границ USB-пакетов. */
PCAN_ABI_API int smg_rtu_response_size(const uint8_t *, size_t); /* Вычисляет размер ответа, исключения либо конфигурации без повторения смещений в Qt-порте. */
PCAN_ABI_API int smg_decode_tcp(const uint8_t *, size_t, smg_request *); /* Декодирует ровно один TCP-запрос без семантического применения команд. */
PCAN_ABI_API int smg_decode_rtu(const uint8_t *, size_t, smg_request *); /* Проверяет CRC до извлечения Unit ID и PDU; CRC реализована в set_crc.c. */
PCAN_ABI_API unsigned smg_check(const smg_request *); /* Проверяет FC01/02/03/04/05/06/15/16, количества и весь диапазон до записи на внешнюю шину. */
PCAN_ABI_API size_t smg_tcp(const smg_request *, uint8_t, const uint8_t *, size_t, uint8_t *, size_t); /* Формирует TCP ADU с исходным Transaction ID и явно выбранным Unit ID. */
PCAN_ABI_API size_t smg_rtu(uint8_t, const uint8_t *, size_t, uint8_t *, size_t); /* Добавляет общий CRC16 к PDU; младший байт CRC передаётся первым. */
PCAN_ABI_API size_t smg_accept_tcp(const smg_request *, uint8_t, const uint8_t *, size_t, uint8_t *, size_t); /* Проверяет корреляцию, длину и echo ответа до передачи PDU клиенту. */
PCAN_ABI_API size_t smg_accept_rtu(const smg_request *, uint8_t, const uint8_t *, size_t, uint8_t *, size_t); /* Проверяет CRC, slave, функцию и форму ответа RTU, включая исключения. */
PCAN_ABI_API size_t smg_exception(const smg_request *, unsigned, uint8_t *); /* Формирует двухбайтный PDU исключения; 0A означает отсутствие пути, 0B отсутствие ответа. */
PCAN_ABI_API void smg_config_default(smg_config *); /* Создаёт остановленную маршрутизацию с 115200 8N1 и тайм-аутом 1000 мс. */
PCAN_ABI_API int smg_config_valid(const smg_config *); /* Проверяет уникальность входных адресов, транспорт и допустимые значения всех маршрутов. */
PCAN_ABI_API size_t smg_config_pack(unsigned, const smg_config *, uint8_t *, size_t); /* Частная FC65: операция 1 читает, 2 применяет, 3 возвращает полную конфигурацию. */
PCAN_ABI_API int smg_config_unpack(const uint8_t *, size_t, unsigned *, smg_config *); /* Разбирает ту же FC65 на ARM и ПК; произвольные shell-команды отсутствуют. */
#ifdef __cplusplus /* Закрывает C ABI при включении из C++. */
} /* Завершает внешнюю C-линковку. */
#endif /* Завершает условие C++-линковки. */
#endif /* Завершает публичный контракт шлюза. */
+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 в первичный загрузчик. */
-88
View File
@@ -1,88 +0,0 @@
#include "set_modbus_gateway.h" /* Общий контракт исключает вторую реализацию форматов в Qt и Linux. */
#include "set_crc.h" /* Использует уже существующую реализацию Modbus CRC16 из templates. */
#include <string.h> /* Копирует только заранее проверенные ограниченные буферы. */
static unsigned get16(const uint8_t *p) { return ((unsigned)p[0] << 8) | p[1]; } /* Пары байтов Modbus всегда передаются старшим байтом вперёд. */
static void put16(uint8_t *p, unsigned v) { p[0] = (uint8_t)(v >> 8); p[1] = (uint8_t)v; } /* Сериализация не зависит от endian ARM или ПК. */
int smg_tcp_size(const uint8_t *p, size_t n) { /* TCP может доставить заголовок несколькими чтениями. */
unsigned length; if (!p) { return -1; } if (n < 6) { return 0; } length = get16(p + 4); /* Шесть байтов достаточно для Protocol ID и длины оставшегося ADU. */
return get16(p + 2) || length < 2 || length > 254 ? -1 : (int)(length + 6); /* Protocol ID равен нулю; длина включает Unit ID и PDU до 253 байт. */
} /* Возвращённый размер позволяет отделять склеенные TCP-запросы без потери хвоста. */
int smg_rtu_size(const uint8_t *p, size_t n) { /* Длина определяется запросом, а не границами USB transfer. */
if (!p) { return -1; } if (n < 2) { return 0; } /* До байта функции нельзя выбрать формат запроса. */
if (p[1] >= 1 && p[1] <= 6) { return 8; } /* FC01…06 содержат адрес, количество либо значение и CRC. */
if (p[1] == 15 || p[1] == 16) { return n < 7 ? 0 : p[6] > 247 ? -1 : 9 + p[6]; } /* Byte count ограничен предельным RTU ADU 256 байт. */
if (p[0] == SMG_CONTROL_UNIT && p[1] == 65) { return n < 3 ? 0 : p[2] == 1 ? 5 : n < 14 ? 0 : p[13] > SMG_ROUTES ? -1 : 16 + 10 * p[13]; } /* FC65: после операции и заголовка следует count маршрутов и два байта CRC. */
return -1; /* Неизвестные RTU-функции требуют границы тишины и проверяются вызывающим портом отдельно. */
} /* Функция не читает ни одного байта за полученным префиксом. */
int smg_rtu_response_size(const uint8_t *p, size_t n) { /* Ответ RTU имеет другой формат длины, чем запрос чтения. */
if (!p) { return -1; } if (n < 2) { return 0; } if (p[1] & 128) { return 5; } /* Все стандартные исключения содержат slave, функцию, код и CRC. */
if (p[1] >= 1 && p[1] <= 4) { return n < 3 ? 0 : p[2] > 250 ? -1 : 5 + p[2]; } /* Ответ чтения содержит byte count и не более 250 байт данных. */
if (p[1] == 5 || p[1] == 6 || p[1] == 15 || p[1] == 16) { return 8; } /* Запись подтверждается фиксированным echo адреса и значения либо количества. */
if (p[0] == SMG_CONTROL_UNIT && p[1] == 65) { return n < 14 ? 0 : p[2] != 3 || p[13] > SMG_ROUTES ? -1 : 16 + 10 * p[13]; } /* Ответ конфигурации содержит операцию 3 и целое число маршрутов. */
return -1; /* Неизвестный ответ не подтверждает незавершённую транзакцию. */
} /* Функция нужна потоковому порту USB и не зависит от размера USB-пакета. */
int smg_decode_tcp(const uint8_t *p, size_t n, smg_request *r) { /* Не принимает неполные ADU и два сообщения как одно. */
int expected = smg_tcp_size(p, n); if (!r || expected < 8 || (size_t)expected != n) { return 0; } /* Пустой PDU не является запросом Modbus. */
memset(r, 0, sizeof(*r)); r->transaction = (uint16_t)get16(p); r->unit = p[6]; r->length = (uint16_t)(n - 7); memcpy(r->pdu, p + 7, r->length); return 1; /* Корреляция клиента отделена от адреса конечного устройства. */
} /* Семантика функции проверяется до выбора внешнего маршрута. */
int smg_decode_rtu(const uint8_t *p, size_t n, smg_request *r) { /* Повреждённые RTU кадры не получают ответа и не меняют состояние. */
if (!p || !r || n < 4 || n > 256 || set_crc16_modbus(p, n)) { return 0; } /* Нулевой остаток CRC проверяет также два переданных байта контрольной суммы. */
memset(r, 0, sizeof(*r)); r->unit = p[0]; r->length = (uint16_t)(n - 3); memcpy(r->pdu, p + 1, r->length); return 1; /* RTU не содержит Transaction ID; порт назначает его для исходящего TCP. */
} /* Исходные байты принадлежат вызывающему и могут быть повторно использованы после возврата. */
unsigned smg_check(const smg_request *r) { /* Ошибочный запрос отклоняется до любого внешнего I/O. */
unsigned fn, address, quantity, bytes, maximum; if (!r || !r->length || r->length > 253) { return 3; } fn = r->pdu[0]; /* Максимальный PDU одинаков для TCP и RTU. */
if (!r->unit) { return 3; } if (fn == 65 && r->unit == SMG_CONTROL_UNIT) { return 0; } /* Широковещательные записи запрещены; адрес 248 обслуживает только настройки шлюза. */
if (!(fn >= 1 && fn <= 6) && fn != 15 && fn != 16) { return 1; } if (r->length < 5) { return 3; } /* Поддерживаются те же базовые функции, что в штатной Modbus-панели SETGUI. */
address = get16(r->pdu + 1); quantity = get16(r->pdu + 3); if (fn <= 6 && r->length != 5) { return 3; } /* Дополнительный хвост не должен скрывать второй запрос. */
if (fn == 5) { return quantity == 0 || quantity == 0xff00 ? 0 : 3; } if (fn == 6) { return 0; } /* FC05 допускает только стандартные значения OFF и ON; FC06 передаёт uint16. */
maximum = fn <= 2 ? 2000 : fn <= 4 ? 125 : fn == 15 ? 1968 : 123; /* Ограничения количеств взяты из описаний FC01/02/03/04/15/16 Modbus V1.1b3. */
if (!quantity || quantity > maximum) { return 3; } if (address + quantity > 65536u) { return 2; } /* Вычисление в unsigned предотвращает 16-битное переполнение диапазона адресов. */
if (fn == 15 || fn == 16) { bytes = fn == 15 ? (quantity + 7) / 8 : quantity * 2; if (r->length != 6 + bytes || r->pdu[5] != bytes) { return 3; } } /* Проверяет весь блок записи до передачи на RS485 либо Ethernet. */
return 0; /* Проверка не применяет значения регистров и не меняет запрос. */
} /* Исключения возвращаются в стандартном PDU, а ошибки CRC отбрасываются раньше. */
size_t smg_tcp(const smg_request *r, uint8_t unit, const uint8_t *pdu, size_t n, uint8_t *out, size_t cap) { /* Один builder используется для запросов и ответов TCP. */
if (!r || !pdu || !out || !n || n > 253 || cap < n + 7) { return 0; } put16(out, r->transaction); put16(out + 2, 0); put16(out + 4, (unsigned)n + 1); out[6] = unit; memcpy(out + 7, pdu, n); return n + 7; /* Возвращает исходную корреляцию независимо от изменения slave на маршруте. */
} /* Protocol ID всегда равен нулю, длина не включает первые шесть байтов MBAP. */
size_t smg_rtu(uint8_t unit, const uint8_t *pdu, size_t n, uint8_t *out, size_t cap) { /* CRC вычисляется только общим кодом templates. */
uint16_t crc; if (!pdu || !out || !n || n > 253 || cap < n + 3) { return 0; } out[0] = unit; memcpy(out + 1, pdu, n); crc = set_crc16_modbus(out, n + 1); out[n + 1] = (uint8_t)crc; out[n + 2] = (uint8_t)(crc >> 8); return n + 3; /* В отличие от регистров, CRC16 передаётся младшим байтом вперёд. */
} /* Unit ID проверяется при выборе маршрута, чтобы адрес управления 248 также мог иметь RTU-ответ. */
static size_t accept_pdu(const smg_request *r, const uint8_t *p, size_t n, uint8_t *out, size_t cap) { /* Не позволяет выдать ответ другого запроса за текущую транзакцию. */
unsigned fn, count, bytes; if (!r || !p || !out || !n || n > cap || smg_check(r)) { return 0; } fn = r->pdu[0]; /* Запрос должен быть предварительно допустимым. */
if (p[0] == (fn | 128u)) { if (n != 2 || !p[1]) { return 0; } } /* Исключение содержит ровно функцию с битом 7 и ненулевой код. */
else if (fn == 65 && r->unit == SMG_CONTROL_UNIT) { smg_config c; unsigned op; if (!smg_config_unpack(p, n, &op, &c) || op != 3) { return 0; } } /* Настройки также проходят проверку общей версией кодека до публикации в GUI. */
else { if (p[0] != fn) { return 0; } count = get16(r->pdu + 3); /* Только ответ исходной функции может завершить ожидание. */
if (fn <= 4) { bytes = fn <= 2 ? (count + 7) / 8 : count * 2; if (n != bytes + 2 || p[1] != bytes) { return 0; } } /* Byte count проверяется по количеству запроса, а не только по длине принятого пакета. */
else if (n != 5 || memcmp(p, r->pdu, 5)) { return 0; } /* FC05/06/15/16 обязаны подтвердить тот же адрес и значение либо количество. */
} memcpy(out, p, n); return n; /* Только полностью проверенный PDU передаётся входному клиенту. */
} /* На неверном ответе выходной буфер остаётся без изменений. */
size_t smg_accept_tcp(const smg_request *r, uint8_t unit, const uint8_t *p, size_t n, uint8_t *out, size_t cap) { /* TCP backend сохраняет единственную исходящую транзакцию. */
smg_request reply; if (!r || !smg_decode_tcp(p, n, &reply) || reply.transaction != r->transaction || reply.unit != unit) { return 0; } return accept_pdu(r, reply.pdu, reply.length, out, cap); /* Проверяет MBAP до проверки формы данных. */
} /* Чужой Transaction ID не подтверждает запись и не вызывает повтор записи. */
size_t smg_accept_rtu(const smg_request *r, uint8_t unit, const uint8_t *p, size_t n, uint8_t *out, size_t cap) { /* RTU backend проверяет целый кадр после межкадровой тишины. */
smg_request reply; if (!r || !smg_decode_rtu(p, n, &reply) || reply.unit != unit) { return 0; } return accept_pdu(r, reply.pdu, reply.length, out, cap); /* Неверная CRC либо адрес не завершают запрос успешным ответом. */
} /* Исключение целевого устройства передаётся без подмены кодом ошибки шлюза. */
size_t smg_exception(const smg_request *r, unsigned code, uint8_t *out) { /* Служба задаёт причину, а формат исключения принадлежит ядру. */
if (!r || !r->length || !out || !code || code > 255) { return 0; } out[0] = r->pdu[0] | 128u; out[1] = (uint8_t)code; return 2; /* Два байта PDU одинаково оборачиваются в TCP и RTU. */
} /* Никакие ошибки внешней шины не превращаются в успешное подтверждение. */
void smg_config_default(smg_config *c) { memset(c, 0, sizeof(*c)); c->baud = 115200; c->timeout_ms = 1000; c->stop_bits = 1; } /* По умолчанию нет маршрутов и нет неявных записей на RS485. */
int smg_config_valid(const smg_config *c) { /* Целиком проверяет таблицу перед публикацией новой конфигурации. */
unsigned i, j; if (!c || c->count > SMG_ROUTES || c->timeout_ms < 100 || c->timeout_ms > 60000 || c->parity > 2 || c->stop_bits < 1 || c->stop_bits > 2 || c->default_serial > 1) { return 0; } /* Тайм-аут выражен в мс и совпадает с допустимым диапазоном SETGUI. */
if (c->baud != 1200 && c->baud != 2400 && c->baud != 4800 && c->baud != 9600 && c->baud != 19200 && c->baud != 38400 && c->baud != 57600 && c->baud != 115200 && c->baud != 230400 && c->baud != 460800 && c->baud != 921600) { return 0; } /* Только скорости, имеющие переносимые Linux termios constants. */
for (i = 0; i < c->count; ++i) { const smg_route *r = c->routes + i; /* Unit ID источника однозначно выбирает одну внешнюю транзакцию. */
if (!r->source || r->source >= SMG_CONTROL_UNIT || !r->target || r->transport > 1 || r->interface_id > 2) { return 0; } /* Адрес управления не может быть переопределён внешним устройством. */
if (!r->transport && (r->target > 247 || r->interface_id || r->ipv4 || r->port)) { return 0; } /* RS485 не принимает сетевые параметры либо широковещательный адрес. */
if (r->transport && (!r->port || !r->ipv4 || (r->ipv4 >> 24) == 0 || (r->ipv4 >> 24) >= 224)) { return 0; } /* Ethernet-маршрут требует unicast IPv4 и ненулевой TCP-порт; loopback допустим для независимых проверок порта. */
for (j = 0; j < i; ++j) if (c->routes[j].source == r->source) { return 0; } /* Повторяющиеся адреса отклоняются вместо неопределённого выбора назначения. */
} return 1; /* Физическое наличие интерфейса дополнительно проверяет порт ОС. */
} /* Ядро не обращается к сети, файловой системе и настройкам UART. */
size_t smg_config_pack(unsigned op, const smg_config *c, uint8_t *p, size_t cap) { /* Один формат обслуживает запись настроек, чтение и файл состояния. */
unsigned i; size_t n; if (!p || op < 1 || op > 3 || cap < 2) { return 0; } if (op == 1) { p[0] = 65; p[1] = 1; return 2; } /* Запрос чтения не передаёт фиктивную конфигурацию. */
if (!smg_config_valid(c)) { return 0; } n = 13 + 10 * c->count; if (cap < n) { return 0; } memset(p, 0, n); p[0] = 65; p[1] = (uint8_t)op; p[2] = 1; put16(p + 3, c->timeout_ms); put16(p + 5, c->baud >> 16); put16(p + 7, c->baud); p[9] = c->parity; p[10] = c->stop_bits; p[11] = c->default_serial; p[12] = c->count; /* Версия 1 фиксирует смещения и единицы измерения без сериализации native-структур. */
for (i = 0; i < c->count; ++i) { const smg_route *r = c->routes + i; uint8_t *at = p + 13 + 10 * i; at[0] = r->source; at[1] = r->target; at[2] = r->transport; at[3] = r->interface_id; put16(at + 4, r->ipv4 >> 16); put16(at + 6, r->ipv4); put16(at + 8, r->port); } return n; /* Каждая запись имеет десять явных байтов независимо от padding C ABI. */
} /* При ошибке ёмкости частичная таблица не формируется. */
int smg_config_unpack(const uint8_t *p, size_t n, unsigned *op, smg_config *c) { /* Разбор выполняется во временную структуру до изменения рабочего состояния. */
smg_config temporary; unsigned i; if (!p || !op || !c || n < 2 || p[0] != 65 || p[1] < 1 || p[1] > 3) { return 0; } if (p[1] == 1) { if (n != 2) { return 0; } *op = 1; return 1; } /* Чтение не изменяет переданный объект конфигурации. */
if (n < 13 || p[2] != 1 || p[12] > SMG_ROUTES || n != 13u + 10u * p[12]) { return 0; } memset(&temporary, 0, sizeof(temporary)); temporary.timeout_ms = (uint16_t)get16(p + 3); temporary.baud = ((uint32_t)get16(p + 5) << 16) | get16(p + 7); temporary.parity = p[9]; temporary.stop_bits = p[10]; temporary.default_serial = p[11]; temporary.count = p[12]; /* Проверяет версию и длину до первого обращения к записи таблицы. */
for (i = 0; i < temporary.count; ++i) { smg_route *r = temporary.routes + i; const uint8_t *at = p + 13 + 10 * i; r->source = at[0]; r->target = at[1]; r->transport = at[2]; r->interface_id = at[3]; r->ipv4 = ((uint32_t)get16(at + 4) << 16) | get16(at + 6); r->port = (uint16_t)get16(at + 8); } /* Распаковывает все адреса, сохраняя одинаковый numeric IPv4 на ПК и ARM. */
if (!smg_config_valid(&temporary)) { return 0; } *c = temporary; *op = p[1]; return 1; /* Транзакционная публикация исключает частичную замену маршрутов при ошибке. */
} /* Протокол управления не содержит имён файлов, команд shell и произвольных путей ОС. */
+37
View File
@@ -0,0 +1,37 @@
#include "c2000_sci.h" /* Проверяет публичный ABI штатного SCI-загрузчика без COM и аппаратного стирания. */
#include <stdlib.h> /* malloc предоставляет корректное выравнивание непрозрачного C-контекста. */
#include <stdio.h> /* Печатает место нарушенного контракта при запуске CTest. */
#include <string.h> /* Сравнивает пакеты с неизменяемыми эталонными октетами. */
#define CHECK(condition) do { if (!(condition)) { fprintf(stderr, "SCI test line %d\n", __LINE__); exit(1); } } while (0) /* Проверки работают также в Release с NDEBUG. */
static const uint8_t ram[] = {0xAA,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x90,2,0,0,0,0,0x90,0x12,0x34,0x56,0x78,0,0}; /* Два слова RAM L4 по адресу 0x9000 и та же точка входа. */
static const uint8_t flash[] = {0xAA,8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0x32,0,0,0x80,2,0,0x32,0,0,0x80,0x12,0x34,0x56,0x78,0,0}; /* Два слова Flash сектора C по адресу 0x328000; ожидаемая маска TI равна 4. */
static void packet(void *session, const uint8_t *expected, size_t length, size_t reply_size, const uint8_t *reply) { /* Проверяет одну эталонную передачу, не собирая пакет повторным кодеком. */
uint8_t tx[2054]; size_t sent, received; uint32_t delay; /* Максимальная порция TI: 1024 слова и заголовок секции из шести октетов. */
CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 1); /* Следующее действие обязано быть обменом, а не пропущенной паузой. */
CHECK(sent == length && received == reply_size && delay == 0 && !memcmp(tx, expected, length)); /* Проверяет точное содержимое, границы и ожидаемый тип ответа. */
if (reply_size) CHECK(c2000_sci_accept(session, reply, reply_size) == 0); /* Только достоверный ответ разрешает последующее действие. */
} /* Завершает проверку одного шага. */
static void pause_ti(void *session) { /* Ожидание проверяется детерминированно без системных часов и реальной трёхсекундной задержки. */
uint8_t tx[1]; size_t sent, received; uint32_t delay; /* Пауза не требует передачи или ответа. */
CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 2 && !sent && !received && delay == 3000); /* Длительность повторяет официальный host TI для Kernel A. */
} /* Завершает проверку обязательной паузы. */
int main(void) { /* Выполняет эталонный сеанс и основные ошибочные случаи на общем C-ядре. */
void *session = malloc(c2000_sci_session_size()); uint8_t changed[sizeof(flash)], tx[2054], decoded[34]; size_t sent, received, written, i; uint32_t delay, entry; uint16_t sectors; /* Хранилище и выходные значения принадлежат host-тесту. */
static const uint8_t sync[] = {'A'}, header_ack[] = {0x64,1}, block_ack[] = {0xC8,1}, monitor[] = {247,213,255,93,0,0,1,0,93,1}; /* Суммы эталона: заголовок 0x0164, секция 0x01C8; Monitor пишет параметр 93 значением 1 и передаёт сумму 255+93+1=349 (0x015D) младшим октетом вперёд. */
static const uint8_t ascii[] = "\002 AA 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 90 02 00 00 00 00 90 12 34 56 78 00 00 \003\r\n"; /* Независимый ASCII-вектор TI содержит STX/ETX, а управляющие октеты не передаются MCU. */
CHECK(session != NULL); CHECK(c2000_sci_validate(ram, sizeof(ram), 0, &entry, &sectors) == 0 && entry == 0x9000 && sectors == 0); /* RAM-ядро должно иметь реально загруженный entry и не включать Flash-секторы. */
CHECK(c2000_sci_validate(flash, sizeof(flash), 1, &entry, &sectors) == 0 && entry == 0x328000 && sectors == 4); /* Прикладной эталон относится именно к сектору C F28335. */
CHECK(c2000_sci_decode(ascii, sizeof(ascii) - 1, decoded, sizeof(decoded), &written) == 0 && written == sizeof(ram) && !memcmp(decoded, ram, written)); /* Проверяет точное совпадение бинарного и ASCII-представления одного образа. */
CHECK(c2000_sci_begin(session, c2000_sci_session_size(), ram, sizeof(ram), flash, sizeof(flash)) == 0); /* Исправные буферы допускают начало autobaud. */
packet(session, sync, 1, 1, sync); /* ROM подтверждает autobaud одним ASCII A. */
for (i = 0; i < sizeof(ram); ++i) packet(session, ram + i, 1, 1, ram + i); /* Первичный загрузчик обязан подтвердить каждый октет RAM-ядра точным эхом. */
pause_ti(session); packet(session, sync, 1, 1, sync); pause_ti(session); /* Ядро запускается и независимо синхронизирует UART перед erase. */
packet(session, flash, 22, 2, header_ack); packet(session, flash + 22, 10, 2, block_ack); packet(session, flash + 32, 2, 0, NULL); /* Проверяет контрольные суммы после erase и программирования, затем терминатор без ACK. */
CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 0 && c2000_sci_progress(session) == 100); /* Только полный подтверждённый сеанс заканчивает прогрессом 100 процентов. */
memcpy(changed, flash, sizeof(flash)); changed[18] = changed[24] = 0x33; changed[20] = changed[26] = 0x80; changed[21] = changed[27] = 0xFF; CHECK(c2000_sci_validate(changed, sizeof(changed), 1, NULL, NULL) == -6); /* Загруженный entry не обходит запрет записи в CSM-область 0x33FF80. */
CHECK(c2000_sci_validate(flash, sizeof(flash) - 2, 1, NULL, NULL) < 0 && c2000_sci_validate(flash, sizeof(flash), 0, NULL, NULL) < 0); /* Усечение таблицы и подмена RAM-ядра Flash-приложением отклоняются. */
CHECK(c2000_sci_begin(session, c2000_sci_session_size(), ram, sizeof(ram), flash, sizeof(flash)) == 0); CHECK(c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == 1); /* Повторный независимый сеанс нужен для проверки неверного эха. */
tx[0] = 'B'; CHECK(c2000_sci_accept(session, tx, 1) == -9 && c2000_sci_next(session, tx, sizeof(tx), &sent, &received, &delay) == -8); /* После ошибки подтверждения автомат не должен продолжать стирание или запись. */
CHECK(c2000_sci_ulitco_enter(tx, sizeof(tx)) == sizeof(monitor) && !memcmp(tx, monitor, sizeof(monitor))); /* Штатный переход Monitor также фиксируется эталонным wire-вектором. */
free(session); return 0; /* Освобождает host-контекст; аппаратный интерфейс нигде не создавался. */
} /* Завершает автономную проверку SCI Boot F28335. */
@@ -1,22 +0,0 @@
#include "set_modbus_gateway.h" /* Проверяет публичный C-контракт той же сборки, что используется GUI и Zynq. */
#include <assert.h> /* Независимые ожидаемые байты и границы должны прерывать выпуск при расхождении. */
#include <stdio.h> /* Единственный PASS печатается после всех протокольных сценариев. */
#include <string.h> /* Сравнивает эталонные wire-байты и неизменность конфигурации при отказе. */
int main(void) { /* Тест не открывает USB, Ethernet или реальные регистры устройств. */
const uint8_t golden_rtu[] = {1,3,0,0,0,10,0xc5,0xcd}, golden_tcp[] = {0x12,0x34,0,0,0,6,1,3,0,0,0,10}; smg_request r, parsed; uint8_t wire[SMG_ADU], pdu[253]; smg_config c, restored, before; unsigned op; size_t n; /* CRC C5 CD задана независимым распространённым вектором FC03. */
assert(smg_decode_rtu(golden_rtu, sizeof(golden_rtu), &r)); r.transaction = 0x1234; assert(!smg_check(&r)); n = smg_tcp(&r, 1, r.pdu, r.length, wire, sizeof(wire)); assert(n == sizeof(golden_tcp) && !memcmp(wire, golden_tcp, n)); /* Перевод RTU → TCP сохраняет PDU и назначенный Transaction ID. */
assert(smg_decode_tcp(golden_tcp, sizeof(golden_tcp), &parsed)); n = smg_rtu(parsed.unit, parsed.pdu, parsed.length, wire, sizeof(wire)); assert(n == sizeof(golden_rtu) && !memcmp(wire, golden_rtu, n)); /* Обратный перевод формирует строго исходный CRC и порядок байтов. */
for (size_t i = 0; i < 6; ++i) assert(!smg_tcp_size(golden_tcp, i)); for (size_t i = 6; i <= sizeof(golden_tcp); ++i) assert(smg_tcp_size(golden_tcp, i) == 12); assert(!smg_decode_tcp(golden_tcp, 11, &parsed)); /* Разделённый MBAP не трактуется как повреждённый или уже завершённый пакет. */
memcpy(wire, golden_tcp, 12); wire[2] = 1; assert(smg_tcp_size(wire, 12) == -1); wire[2] = 0; wire[4] = 1; wire[5] = 255; assert(smg_tcp_size(wire, 12) == -1); /* Некорректный Protocol ID и чрезмерная длина отклоняются до копирования. */
memcpy(wire, golden_rtu, 8); wire[7] ^= 1; assert(!smg_decode_rtu(wire, 8, &parsed)); assert(!smg_tcp(&r, 1, r.pdu, r.length, wire, 11)); /* Ошибка CRC и недостаточная ёмкость не порождают валидный запрос. */
r.pdu[3] = 0; r.pdu[4] = 0; assert(smg_check(&r) == 3); r.pdu[4] = 126; assert(smg_check(&r) == 3); r.pdu[4] = 2; r.pdu[1] = 255; r.pdu[2] = 255; assert(smg_check(&r) == 2); /* Количество и адресный wraparound проверяются до внешней операции. */
r.pdu[0] = 5; r.pdu[3] = 1; assert(smg_check(&r) == 3); r.pdu[3] = 255; r.pdu[4] = 0; assert(!smg_check(&r)); /* FC05 принимает стандартные FF00 и 0000, а не произвольную маску. */
r.pdu[0] = 16; r.pdu[1] = r.pdu[2] = r.pdu[3] = 0; r.pdu[4] = 2; r.pdu[5] = 4; r.length = 10; assert(!smg_check(&r)); r.pdu[5] = 3; assert(smg_check(&r) == 3); /* Весь блок FC16 валидируется до передачи, включая byte count. */
r.pdu[5] = 4; n = smg_exception(&r, 11, pdu); assert(n == 2 && pdu[0] == 0x90 && pdu[1] == 11); n = smg_rtu(7, pdu, n, wire, sizeof(wire)); assert(smg_accept_rtu(&r, 7, wire, n, pdu, sizeof(pdu)) == 2); assert(!smg_accept_rtu(&r, 8, wire, n, pdu, sizeof(pdu))); /* Исключение устройства передаётся только при совпадении конечного slave. */
const uint8_t echo[] = {16,0,0,0,2}; n = smg_tcp(&r, 7, echo, sizeof(echo), wire, sizeof(wire)); assert(smg_accept_tcp(&r, 7, wire, n, pdu, sizeof(pdu)) == 5); wire[0] ^= 1; assert(!smg_accept_tcp(&r, 7, wire, n, pdu, sizeof(pdu))); wire[0] ^= 1; wire[n - 1] = 3; assert(!smg_accept_tcp(&r, 7, wire, n, pdu, sizeof(pdu))); /* Чужой Transaction ID и подтверждение другого количества не принимаются. */
smg_config_default(&c); assert(smg_config_valid(&c) && !c.default_serial && !c.count); c.count = 2; c.routes[0] = (smg_route){5,1,0,0,0,0}; c.routes[1] = (smg_route){6,1,1,2,0xc0a8010a,502}; n = smg_config_pack(2, &c, pdu, sizeof(pdu)); assert(n == 33 && smg_config_unpack(pdu, n, &op, &restored) && op == 2 && !memcmp(&c, &restored, sizeof(c))); /* Таблица одновременно содержит RS485 и Ethernet RJ2 с remap двух одинаковых Slave ID. */
before = restored; pdu[23] = 5; assert(!smg_config_unpack(pdu, n, &op, &restored) && !memcmp(&before, &restored, sizeof(before))); /* Дубликат источника отвергается транзакционно, не затирая действующую таблицу. */
c.count = SMG_ROUTES; for (unsigned i = 0; i < c.count; ++i) c.routes[i] = (smg_route){(uint8_t)(i + 1),1,0,0,0,0}; n = smg_config_pack(3, &c, pdu, sizeof(pdu)); assert(n == 213 && smg_config_unpack(pdu, n, &op, &restored)); n = smg_rtu(SMG_CONTROL_UNIT, pdu, n, wire, sizeof(wire)); assert(n == 216 && smg_rtu_response_size(wire, n) == 216 && smg_rtu_size(wire, n) == 216); /* Максимальная таблица помещается в один стандартный RTU ADU. */
r.unit = SMG_CONTROL_UNIT; r.length = (uint16_t)smg_config_pack(1, NULL, r.pdu, sizeof(r.pdu)); assert(smg_accept_rtu(&r, SMG_CONTROL_UNIT, wire, n, pdu, sizeof(pdu)) == 213); /* Ответ FC65 проходит тот же CRC и потоковый USB-контракт, что обычные данные. */
puts("PASS modbus gateway: golden TCP/RTU, bounds, CRC, correlation, configuration"); return 0; /* PASS относится только к завершённым проверкам общего ядра. */
} /* Аппаратные тайминги и работа конкретного PHY проверяются отдельными сценариями. */
+1 -1
View File
@@ -24,7 +24,7 @@ INCLUDE = ROOT / "include"
JNI_INCLUDES: list[Path] = []
SOURCES = [
ROOT / "src" / name for name in (
"set_modbus_gateway.c", # Экспортирует общий USB RTU/TCP кодек также при прямой MSVC/CC сборке без CMake.
"c2000_sci.c", # Прямые MSVC/CC-сборки экспортируют общий автомат штатного SCI-загрузчика F28335.
"deif_xdi.c", # Общий DEIF входит в host-библиотеку.
"set_invertor_gas.c", # Собирает общий адаптер двух GAS для STM Invertor при прямом запуске host-builder.
"set_deif_gauge.c", "set_igbt_realtime.c",
-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 и транзакционность конфигурации.
+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;
File diff suppressed because one or more lines are too long
+1 -6
View File
@@ -468,12 +468,7 @@ class Rs485PacketParser:
# Команды прошивки
# ==========================================================================
def command_can_interface(name: str) -> str: # Расширение общего контракта выбирает PS CAN Linux, сохраняя protocol wire в templates.
if name not in ('can0', 'can1'): raise ValueError('CAN-интерфейс должен быть can0 или can1') # Закрытый набор не допускает произвольных имён и внедрения дополнительных команд.
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-107
View File
@@ -1,107 +0,0 @@
"""Единый неблокирующий порт CAN485 для USB CDC/UART и Ethernet TCP.""" # Протокол кадров остаётся в can485_board и переносимом C-ядре templates.
from .qt_compat import QObject, Signal, QTimer, QSerialPort, QElapsedTimer # Qt elapsed timer ограничивает также короткое подтверждение восстановления UART-консоли.
try: # Выбирает тот же Qt binding, который использует остальное приложение.
from PySide6.QtNetwork import QTcpSocket, QAbstractSocket # TCP передаёт тот же поток строк, что физический USB-порт.
except ImportError: # Старые системы используют доступный PySide2.
from PySide2.QtNetwork import QTcpSocket, QAbstractSocket # Публичный контракт адаптера не зависит от версии Qt.
class Can485Port(QObject): # Совместимая с BoardTab оболочка исключает копирование протокольных и экранных обработчиков.
readyRead = Signal() # Уведомляет монитор о произвольном фрагменте, включая неполную строку.
errorOccurred = Signal(object) # Использует существующий обработчик ошибок последовательного порта панели.
connected = Signal() # TCP-подключение подтверждается асинхронно после фактического установления соединения.
def __init__(self, parent=None): # Создаёт закрытые endpoints без аппаратного ввода-вывода при импорте или открытии вкладки.
super().__init__(parent) # QObject связывает жизненный цикл сокетов с владельцем панели.
self.serial = QSerialPort(self); self.tcp = QTcpSocket(self); self.kind = 'USB'; self._closing = False; self._error = ''; self._endpoint = ''; self._console = False; self._console_ready = False; self._console_rx = bytearray(); self.console_restored=True # Состояние восстановления консоли проверяется аппаратным USB-тестом после закрытия.
self.serial.readyRead.connect(self._serial_ready); self.tcp.readyRead.connect(self.readyRead) # USB-консоль ожидает подтверждение службы, затем передаёт bytes штатному parser строк.
self.serial.errorOccurred.connect(self._serial_error); self.tcp.errorOccurred.connect(self._tcp_error); self.tcp.disconnected.connect(self._disconnected) # Потеря соединения останавливает циклическую отправку через обработчик панели.
self.tcp.connected.connect(self._connected) # Начальная настройка выполняется только после подтверждённого TCP handshake.
self.timeout = QTimer(self); self.timeout.setSingleShot(True); self.timeout.timeout.connect(self._timed_out) # Конечный срок TCP не блокирует GUI даже при отключённой плате.
self._shared=None # Единая сборка позволяет CAN и Modbus одновременно использовать один физический FTDI.
def _connected(self): # Завершает фазу ожидания и разрешает запрос версии.
self.timeout.stop(); self.connected.emit() # Запоздалый timeout больше не может закрыть действующую сессию.
def _tcp_error(self, error): # Преобразует ошибку TCP в общий контракт порта.
if not self._closing: self._error = self.tcp.errorString(); self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # GUI получает исходную диагностическую строку сокета.
def _serial_error(self, error): # Сохраняет штатные коды QSerialPort, включая отсутствие ошибки.
if not self._closing: self._error = self.serial.errorString(); self.errorOccurred.emit(error) # USB-обрыв проходит тот же cleanup, что сетевой.
def _disconnected(self): # FIN от платы завершает сессию даже без отдельного socket error.
if not self._closing: self._error = 'Ethernet: соединение закрыто платой'; self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # Панель прекращает периодическую передачу и очищает подтверждённые настройки.
def _timed_out(self): # Срабатывает только для незавершённого TCP-подключения.
if not self.isOpen(): self._error = 'Нет ответа CAN-службы Zynq за 5 секунд. Для USB-консоли нужны root shell и установленная zynq-can485d.'; self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # Ожидание отсутствующего сервиса либо login prompt не блокирует GUI.
def _serial_ready(self): # В режиме консоли отделяет готовность туннеля от простого открытия COM-порта.
if self._closing: return # Во время короткого ожидания shell prompt данные читает cleanup, не вызывая повторные GUI callbacks.
if not self._console: self.readyRead.emit(); return # Прямой USB CDC/UART сохраняет исходные bytes без буферной копии.
self._console_rx.extend(bytes(self.serial.readAll())) # Shell и nc могут выдавать произвольные фрагменты строк.
if not self._console_ready and b'#INFO Zynq CAN485 / templates' in self._console_rx: self._console_ready=True; self.timeout.stop(); self.connected.emit() # Команды CAN разрешаются только после реального ответа общего daemon.
if self._console_ready: self.readyRead.emit() # После подтверждения весь поток, включая сохранённый заголовок, поступает штатному монитору.
elif len(self._console_rx)>8192: del self._console_rx[:-4096] # До handshake хранит ограниченный хвост консольного вывода.
def start_console_tunnel(self, port=37579): # Использует уже открытую root UART-консоль как USB-канал к тому же TCP-порту службы.
port=int(port) # Числовой аргумент проверяется до формирования команды shell.
if not 1<=port<=65535: raise ValueError('TCP-порт CAN должен быть 1..65535') # Исключает неверный endpoint и внедрение shell-команд из параметра.
self._console=True; self._console_ready=False; self._console_rx.clear(); self.console_restored=False; self.timeout.start(5000) # Открытый tty не выдаётся за подтверждённый CAN-сеанс.
self.serial.write(f'\rstty -echo -icanon min 1 time 0; nc 127.0.0.1 {port}; stty sane\r'.encode('ascii')) # Штатный nc передаёт общий протокол без копии daemon и без перенастройки USB/Device Tree.
def connect_tcp(self, host, port): # Endpoint уже проверен UI, DNS и синхронное ожидание не требуются.
self.close(); self.kind = 'Ethernet'; self._endpoint = f'{host}:{port}'; self._error = ''; self.timeout.start(5000); self.tcp.connectToHost(host, port) # Пять секунд ограничивают ожидание недоступного TCP-порта 37579.
def connect_console(self,name,port=37579): # Общий FTDI канал открывает отдельную CAN TCP сессию без захвата COM второй вкладкой.
from .console_link_port import ConsoleLink # Общий менеджер делится с Modbus и сохраняет действующий USB host HDMI.
self.close(); self.kind='USB'; self._endpoint=name; self._error=''; self.console_restored=False; self._shared=ConsoleLink.acquire(name,port,self); channel=self._shared; self.timeout.start(5000) # Начальное открытие остаётся отменяемым до подтверждения backend.
channel.readyRead.connect(self.readyRead); channel.opened.connect(self._connected); channel.errorOccurred.connect(lambda error:self._shared_error(channel,error)) # Прикладные CAN строки и ошибки принадлежат только своему логическому каналу.
def _shared_error(self,channel,error): # Ошибка локального CAN backend не должна выдавать сообщение физического QtSerialPort другого протокола.
if not self._closing: self._error=channel.errorString(); self.errorOccurred.emit(error) # Стандартный BoardTab cleanup завершает только CAN сессию.
def isOpen(self): # Штатные кнопки BoardTab учитывают реальное состояние выбранного канала.
if self._shared is not None: return self._shared.active and self._shared.isOpen() # Общий COM не считается готовым CAN до OPEN конкретного loopback backend.
return self.tcp.state() == QAbstractSocket.SocketState.ConnectedState if self.kind == 'Ethernet' else self.serial.isOpen() and (not self._console or self._console_ready) # Открытый USB tty с незавершённым nc handshake тоже не считается CAN-подключением.
def isConnecting(self): # Позволяет отменить незавершённую попытку подключения повторным нажатием кнопки.
return self.timeout.isActive() # Таймер активен только в фазе TCP handshake.
def close(self): # Закрытие идемпотентно и не порождает рекурсивную ошибку панели.
self._closing = True; self.timeout.stop(); self.tcp.abort() # Сначала отменяет deadline и сетевые сигналы старой сессии.
if self._shared is not None: channel=self._shared; self._shared=None; channel.close(); self.console_restored=channel.link.console_restored; channel.deleteLater() # Освобождается логический CAN канал, тогда как Modbus продолжает использовать тот же FTDI.
if self._console and self.serial.isOpen(): # Дожидается реального ответа UART, чтобы close не отбросил ещё не переданный Ctrl-C в драйвере Windows.
self.serial.readAll(); self.serial.write(b'\x03'); self.serial.flush() # Удаляет старый поток статуса и немедленно передаёт сигнал завершения собственного foreground nc.
if self.serial.bytesToWrite(): self.serial.waitForBytesWritten(150) # Ограничивает ожидание освобождения локальной выходной очереди.
elapsed=QElapsedTimer(); elapsed.start(); response=bytearray() # Подтверждение восстановления shell имеет конечный срок 400 мс.
while elapsed.elapsed()<400 and not self.console_restored: # Нельзя закрыть FTDI раньше передачи управляющего байта и возврата root prompt.
self.serial.waitForReadyRead(40); response.extend(bytes(self.serial.readAll())); self.console_restored=response.rstrip(b'\r\n').endswith((b'# ',b'$ ')) # Настоящий prompt подтверждает завершение nc и выполнение stty sane.
if not self.console_restored: self._error='Закрыт USB, но возврат Linux-консоли за 400 мс не подтверждён' # Не выдаёт простое закрытие COM за подтверждённое восстановление терминала.
if self.serial.isOpen(): self.serial.close() # Не вызывает NotOpenError при повторном cleanup уже закрытого USB-порта.
self._console=False; self._console_ready=False; self._console_rx.clear(); self._closing = False # Не оставляет старые данные handshake для следующего COM-порта.
def readAll(self): # Возвращает сырой QByteArray для существующего LineAssembler templates.
if self._shared is not None: return self._shared.readAll() # Общий физический поток уже разделён на независимые CAN/Modbus сессии.
if self._console: data=bytes(self._console_rx); self._console_rx.clear(); return data # Консольные bytes прочитаны единожды при поиске подтверждённого заголовка.
return self.tcp.readAll() if self.kind == 'Ethernet' else self.serial.readAll() # Сегментация TCP/USB не влияет на протокол.
def write(self, data): # Длина результата позволяет обнаружить непринятую целиком команду.
if self._shared is not None: return self._shared.write(data) # Канал кодирует только транспортное обрамление, не меняя исходные CAN команды.
return self.tcp.write(data) if self.kind == 'Ethernet' else self.serial.write(data) # Порт не добавляет заголовков либо переводов строк к готовой команде.
def errorString(self): # Единая диагностика сохраняется до следующего соединения.
return self._error or (self.tcp.errorString() if self.kind == 'Ethernet' else self.serial.errorString()) # Сообщение Qt не подменяется общим «нет связи».
def portName(self): # Нижняя строка монитора показывает фактический канал.
if self._shared is not None: return self._shared.portName() # Общий COM остаётся видимым пользователю при одновременной работе двух протоколов.
return self._endpoint if self.kind == 'Ethernet' else self.serial.portName() # Сетевое соединение отличается от COM-порта платы.
def setPortName(self, name): # Штатный USB-путь BoardTab выбирает физический порт без изменения своего кода.
self.kind = 'USB'; self.serial.setPortName(name) # Следующие настройки применяются только к закрытому USB-порту.
def setBaudRate(self, value): return self.serial.setBaudRate(value) # Физический USB-UART требует согласованной скорости, USB CDC принимает line coding.
def setDataBits(self, value): return self.serial.setDataBits(value) # Использует 8 бит данных штатной панели.
def setParity(self, value): return self.serial.setParity(value) # Сохраняет отсутствие parity в общем контракте 8N1.
def setStopBits(self, value): return self.serial.setStopBits(value) # Сохраняет один стоповый бит.
def setFlowControl(self, value): return self.serial.setFlowControl(value) # RTS/CTS не задействуются для управления платой.
def open(self, mode): return self.serial.open(mode) # Открытие USB выполняется штатным QSerialPort с исходной диагностикой ошибок.
@@ -1,107 +0,0 @@
"""Общий владелец FTDI и независимые логические TCP-каналы CAN/Modbus поверх Linux shell.""" # Прикладные кодеки не зависят от способа совместного использования COM.
from PySide6.QtCore import QObject, Signal, QTimer, QByteArray, QCoreApplication, QElapsedTimer, QIODevice # Один Qt event loop обслуживает все логические каналы без фоновых гонок.
from PySide6.QtSerialPort import QSerialPort # Физический COM открывается ровно одним экземпляром менеджера.
import shlex # Путь установленного либо явно выбранного тестового helper передаётся shell одним безопасным аргументом.
import re # Подтверждённый root prompt отделяет shell от login/getty и запроса пароля после холодной загрузки.
class ConsoleChannel(QObject): # Контракт readAll/write/close позволяет подключить существующие thin-порты протоколов.
readyRead=Signal(); opened=Signal(); errorOccurred=Signal(object) # OPEN подтверждается loopback службой отдельно от открытия FTDI.
def __init__(self,link,number,port,parent): # Канал принадлежит конкретному потребителю, физическая сессия — приложению.
super().__init__(parent); self.link=link; self.number=number; self.port=port; self.active=False; self.closed=False; self.incoming=bytearray(); self.error='' # Соседние протоколы имеют независимые буферы и состояния.
def isOpen(self): return not self.closed # Ожидающую handshake сессию можно отменить той же кнопкой отключения.
def portName(self): return self.link.name # Панель показывает реальный COM, общий для обеих служб платы.
def errorString(self): return self.error # Ошибка сохраняется до уничтожения конкретной логической сессии.
def readAll(self): data=QByteArray(bytes(self.incoming)); self.incoming.clear(); return data # Каждый прикладной байт читается только своим протоколом.
def write(self,data): # Возвращает длину принятых в очередь данных без подтверждения внешней операции.
raw=bytes(data) # QByteArray и bytes преобразуются без изменения бинарного содержимого.
if self.closed or not self.active: return -1 # Команда не уходит в неподтверждённый или уже закрытый канал.
for offset in range(0,len(raw),512): # Короткая строка HEX позволяет чередовать CAN и Modbus при 115200 бод.
if not self.link.send(f'D{self.number:02X}:'.encode('ascii')+raw[offset:offset+512].hex().encode('ascii')+b'\n'): return -1 # Переполнение физической очереди прекращает сессию без повтора команды.
return len(raw) # Успех означает только локальную постановку в очередь QSerialPort.
def close(self): # Закрытие протокола не отбирает общий COM у оставшихся потребителей.
if self.closed: return # Идемпотентный cleanup безопасен при синхронных Qt error callbacks.
self.closed=True; self.active=False; self.incoming.clear(); self.link.release(self) # Менеджер завершает shell helper только после последнего канала.
def failed(self,message): # Ошибка одного backend не маскируется состоянием другого протокола.
if self.closed: return # Запоздалое событие не возобновляет отключённую страницу.
self.error=message; self.close(); self.errorOccurred.emit(QSerialPort.SerialPortError.ResourceError) # Потребитель очищает собственные незавершённые запросы.
class ConsoleLink(QObject): # Один экземпляр на имя COM обслуживает сколько угодно независимых backend сессий.
links={} # Реестр процесса сохраняет одного физического владельца между вкладками SETGUI.
program='/opt/zynq-link/console_link.py' # Проверка staged-пакета может выбрать его точный локальный путь до постоянной установки helper.
@classmethod # Потребитель запрашивает канал, не управляя временем жизни общей UART-консоли.
def acquire(cls,name,port,parent=None): # Сетевой адрес не передаётся из UI: разрешены только две локальные службы платы.
if int(port) not in (37579,502): raise ValueError('Общий USB поддерживает CAN 37579 и Modbus 502') # Разрешены штатный CAN и стандартный Modbus TCP, без произвольного loopback proxy.
name=str(name).strip(); key=name.upper() if name.upper().startswith('COM') else name # Windows COM нечувствителен к регистру, Linux PTY сохраняет точное имя.
link=cls.links.get(key) # Повторная вкладка присоединяется к уже открытому физическому каналу.
if link is None: link=cls(name,key); cls.links[key]=link; QTimer.singleShot(0,link.start) # Асинхронный запуск даёт потребителю время подключить error/opened callbacks.
number=next((value for value in range(1,256) if value not in link.used),None) # Номер не переиспользуется до закрытия физической сессии, поэтому поздний DATA старого клиента не попадёт новому.
if number is None: raise RuntimeError('Исчерпаны USB-каналы платы') # Ограничение совпадает с однобайтовым номером общего протокола.
channel=ConsoleChannel(link,number,int(port),parent); link.channels[number]=channel; link.used.add(number) # Множество использованных номеров защищает от смешения с поздними queued ответами закрытого backend.
if link.ready: QTimer.singleShot(0,lambda:link.open_channel(channel)) # Существующий helper открывает только новый backend без повторной shell команды.
return channel # Никакая прикладная команда ещё не отправляется в CAN или Modbus.
def __init__(self,name,key): # Общая сессия переживает переключение и закрытие одной рабочей страницы.
super().__init__(QCoreApplication.instance()); self.name=name; self.key=key; self.serial=QSerialPort(self); self.channels={}; self.used=set(); self.incoming=bytearray(); self.ready=False; self.closing=False; self.console_restored=True # QObject приложения освобождает менеджер до завершения Qt runtime, а история номеров изолирует поздние DATA.
self.timer=QTimer(self); self.timer.setSingleShot(True); self.timer.timeout.connect(lambda:self.fail('Нет ответа общего USB-туннеля: проверьте root shell и /opt/zynq-link/console_link.py')) # Ошибка начального handshake имеет конечный срок 5 секунд.
self.serial.readyRead.connect(self.receive); self.serial.errorOccurred.connect(self.serial_error) # Единственный readyRead читает физические байты для всех протоколов.
def start(self): # FTDI открывается только после явного запроса первой страницы подключения.
if self.closing or not self.channels: return # Закрытый до первого event loop запрос не занимает COM.
self.serial.setPortName(self.name); self.serial.setBaudRate(115200); self.serial.setDataBits(QSerialPort.DataBits.Data8); self.serial.setParity(QSerialPort.Parity.NoParity); self.serial.setStopBits(QSerialPort.StopBits.OneStop); self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) # Параметры соответствуют проверенной root-консоли платы, независимо от внешней RS485.
if not self.serial.open(QIODevice.OpenModeFlag.ReadWrite): self.fail(self.serial.errorString()); return # Занятый сторонним терминалом COM не перехватывается.
self.console_restored=False; self.phase='shell'; self.login_sent=False; self.cursor_replied=False; self.timer.start(5000); self.send(b'\x15\r') # Сначала запрашивается фактический prompt: после холодного boot COM может ещё принадлежать getty, а не shell.
def start_helper(self): # Команды tty разрешены только после подтверждения root shell известной платы.
self.phase='helper'; self.incoming.clear(); self.send(("stty -echo -icanon -ixon -ixoff min 1 time 0; python3 "+shlex.quote(self.program)+"; stty sane; printf '\\n#ZYNQ-LINK-CLOSED\\n'\r").encode('ascii')) # Путь экранирован одним аргументом, завершение helper восстанавливает обычный терминал.
def send(self,data): # Общая очередь Windows ограничена независимо от скорости прикладных клиентов.
if self.closing or not self.serial.isOpen(): return False # Закрытый physical fd не накапливает команды для будущего запуска.
if self.serial.bytesToWrite()+len(data)>262144 or self.serial.write(data)!=len(data): self.fail('Переполнена очередь общего USB; уменьшите поток CAN/Modbus'); return False # Предел 256 КиБ не допускает бесконечного отставания от UART.
return True # Реальное подтверждение операции остаётся обязанностью прикладного протокола.
def open_channel(self,channel): # Каждый backend соединяется отдельно внутри уже работающего общего helper.
if not channel.closed and self.ready: self.send(f'O{channel.number:02X}:{channel.port}\n'.encode('ascii')) # Номер и порт сформированы из проверенных локальных чисел.
def receive(self): # Произвольные USB фрагменты собираются в ограниченные ASCII строки обрамления.
if self.closing: return # Во время короткого cleanup принятые байты проверяет finish, избегая рекурсии callback.
self.incoming.extend(bytes(self.serial.readAll())) # Физический поток читается ровно одним QObject.
if len(self.incoming)>16384: self.fail('Неверный поток общего USB-туннеля'); return # Ни shell, ни повреждённый backend не могут бесконечно накапливать данные.
if not self.ready and self.phase=='shell': # Login и prompt не обязаны оканчиваться переводом строки, поэтому проверяются до разбора multiplex строк.
if b'Password:' in self.incoming: self.fail('Консоль платы требует пароль: войдите через терминал и повторите подключение'); return # Пароль не угадывается и не отправляется автоматически.
if b'\x1b[6n' in self.incoming and not self.cursor_replied: self.cursor_replied=True; self.send(b'\x1b[24;80R') # Factory autologin вызывает resize с ANSI запросом курсора; стандартный ответ 24 строки и 80 столбцов освобождает shell без ввода фиктивной команды.
plain=re.sub(rb'\x1b(?:\[[0-?]*[ -/]*[@-~]|[78])',b'',bytes(self.incoming)) # ANSI save/restore cursor и resize не являются частью реального root prompt.
if re.search(rb'(?:^|[\r\n])root@image_7020:[^\r\n]*#[ \t]*$',plain): self.start_helper(); return # Только фактическая root shell разрешает запуск общего helper после штатного factory autologin.
if re.search(rb'login:[ \t]*$',plain) and not self.login_sent: self.login_sent=True; self.incoming.clear(); self.send(b'root\r') # Известное имя отправляется только getty, действительно ожидающему ввод; строка automatic login не вызывает лишнюю root команду.
return # Текст getty не попадает в Modbus/CAN parser и не считается banner helper.
while b'\n' in self.incoming: # Одно уведомление может содержать ответы нескольких логических каналов.
raw,_,tail=self.incoming.partition(b'\n'); self.incoming[:]=tail; line=raw.rstrip(b'\r') # Терминальные CR не входят в HEX данные прикладных протоколов.
if not self.ready: # Вывод shell и echo стартовой команды не становятся Modbus ответом.
if line==b'#ZYNQ-LINK 1': self.ready=True; self.timer.stop(); [self.open_channel(channel) for channel in list(self.channels.values())] # Только настоящий banner разрешает OPEN backend каналов.
continue # Старый консольный вывод не передаётся ни одному прикладному parser.
if len(line)<3: continue # Пустые строки не влияют на состояние готовых каналов.
try: number=int(line[1:3],16) # Однобайтовый номер позволяет выбрать единственного получателя.
except ValueError: self.fail('Повреждено обрамление общего USB'); return # Ошибка multiplex framing закрывает физическую сессию без повторов.
channel=self.channels.get(number) # Закрытый потребитель может оставить поздние байты, которые следует игнорировать.
if channel is None: continue # Поздний DATA не создаёт сессию автоматически.
if line[:1]==b'O' and len(line)==3: channel.active=True; channel.opened.emit() # Connected относится к установленному loopback соединению конкретной службы.
elif line[:1]==b'C' and len(line)==3: channel.failed('Служба платы закрыла USB-канал') # FIN одного сервиса не отключает остальные протоколы.
elif line[:1]==b'E' and line[3:4]==b':': channel.failed('Ошибка USB-канала службы: errno '+line[4:].decode('ascii','replace')) # Числовая причина Linux доступна рядом с соответствующей вкладкой.
elif line[:1]==b'D' and line[3:4]==b':': # Прикладные данные раскодируются только после выбора активного канала.
try: data=bytes.fromhex(line[4:].decode('ascii')) # Все управляющие значения байта остаются неизменными данными TCP потока.
except (ValueError,UnicodeError): self.fail('Повреждены HEX данные общего USB'); return # Неисправное обрамление не проходит к Modbus C-кодеку как частичный успех.
if len(channel.incoming)+len(data)>65536: channel.failed('Переполнен буфер USB-канала'); continue # Ограничение одного потребителя не мешает работе остальных.
channel.incoming.extend(data); channel.readyRead.emit() # Существующий parser получает произвольный фрагмент исходного TCP.
else: self.fail('Неизвестный ответ общего USB-туннеля'); return # Неожиданное возвращение shell не выдаётся за действующий протокольный поток.
def serial_error(self,error): # Qt NoError не считается отказом физического FTDI.
if not self.closing and self.serial.isOpen() and error not in (QSerialPort.SerialPortError.NoError,QSerialPort.SerialPortError.TimeoutError): self.fail(self.serial.errorString()) # Отключение кабеля завершает все каналы одной общей сессии.
def release(self,channel): # Канал удаляется до посылки CLOSE, исключая повторную обработку поздних данных.
self.channels.pop(channel.number,None) # Повторный cleanup не затрагивает другой номер сессии.
if self.ready and not self.closing: self.send(f'C{channel.number:02X}\n'.encode('ascii')) # Backend получает штатный обрыв собственного TCP соединения.
if not self.channels: self.finish() # Последняя страница возвращает обычную Linux-консоль и освобождает COM.
def fail(self,message): # Физическая ошибка закрывает всех владельцев без автоматического повторного запуска.
if self.closing: return # Синхронный QSerialPort error во время close не вызывает рекурсию.
channels=list(self.channels.values()); self.closing=True # Сначала запрещаются новые записи всех потребителей.
for channel in channels: channel.failed(message) # Каждая вкладка получает собственный cleanup и прекращает опрос.
self.finish() # Общий UART освобождается после доставки ошибок всем потребителям.
def finish(self): # Только последний потребитель завершает собственный foreground helper.
self.closing=True; self.timer.stop() # Остальные Qt callbacks не читают tty во время подтверждения возврата shell.
if self.serial.isOpen(): # Отсоединённый физический кабель не заставляет ждать несуществующее подтверждение.
self.serial.readAll(); self.serial.write(b'\x03'); self.serial.flush() # ISIG остаётся включённым, поэтому Ctrl-C завершает только собственную foreground группу helper.
if self.serial.bytesToWrite(): self.serial.waitForBytesWritten(150) # Очередь передачи ограничена коротким ожиданием фактической отправки Ctrl-C.
elapsed=QElapsedTimer(); elapsed.start(); response=bytearray() # Консоль должна подтвердить выполнение stty sane за ограниченные 500 мс.
while elapsed.elapsed()<500 and not self.console_restored: self.serial.waitForReadyRead(25); response.extend(bytes(self.serial.readAll())); self.console_restored=b'\n#ZYNQ-LINK-CLOSED\r' in response or b'\n#ZYNQ-LINK-CLOSED\n' in response # Собственный marker отличает реальный возврат shell от простого закрытия COM.
self.serial.close() # Сторонний терминал может открыть COM после завершения последнего протокола.
if self.links.get(self.key) is self: self.links.pop(self.key) # Новый запрос пользователя создаст новую чистую физическую сессию.
self.deleteLater() # Удаление QObject выполняется штатным Qt event loop до выгрузки runtime.
@@ -1,92 +0,0 @@
"""Асинхронный USB-порт шлюза и локальный TCP endpoint для существующих Modbus-клиентов.""" # Wire-кодек и проверка ответов выполняются только C-ядром templates.
from collections import deque # Ограниченная последовательная очередь исключает перемешивание RTU-ответов разных клиентов.
import ctypes as ct # Передаёт native-запрос в общую C-проверку до отправки по USB.
from PySide6.QtCore import QObject, QIODevice, QTimer, Signal # Жизненный цикл и дедлайны остаются в GUI event loop без фоновых гонок.
from PySide6.QtNetwork import QHostAddress, QTcpServer # Loopback связывает существующий Qt Modbus-клиент с единственным владельцем USB.
from PySide6.QtSerialPort import QSerialPort # Выделенный USB CDC COM открывается только по действию пользователя.
from ..modbus_gateway import GatewayCodec # Python не вычисляет CRC, MBAP, смещения конфигурации и правила корреляции.
class ModbusGatewayPort(QObject): # Один экземпляр обслуживает настройки и опрос через один физический USB endpoint.
state = Signal(str) # Сообщает текущую фазу подключения и подтверждённые ошибки транспорта.
configuration = Signal(object) # Передаёт только прочитанную с Zynq конфигурацию ABI.
connected = Signal(bool) # Позволяет UI закрыть встроенный опрос при потере USB.
def __init__(self, parent=None, codec=None): # Тест может предоставить библиотеку конкретной проверяемой сборки.
super().__init__(parent); self.codec = codec or GatewayCodec(); self.serial = QSerialPort(self); self.server = QTcpServer(self); self.queue = deque(); self.pending = None; self.buffers = {}; self.received = bytearray(); self.config = self.codec.default(); self.ready = False; self.closing = False # Все очереди и callbacks принадлежат одному Qt-потоку.
self.direct=self.serial; self.tcp_transport=False # Прямой CDC сохраняет RTU, а общий FTDI канал переносит стандартный Modbus TCP к той же службе.
self.timer = QTimer(self); self.timer.setSingleShot(True); self.timer.timeout.connect(lambda: self.fail('Тайм-аут USB; проверьте OTG-порт и службу шлюза на Zynq')) # После тайм-аута сессия закрывается, чтобы поздний RTU-ответ не подтвердил следующий запрос.
self.serial.readyRead.connect(self._receive); self.serial.errorOccurred.connect(self._serial_error); self.server.newConnection.connect(self._accept) # Уведомления не запускают блокирующих read/waitForReadyRead.
@property # Локальный endpoint назначает ОС после успешного чтения настроек платы.
def port(self): return self.server.serverPort() # Нулевой порт до подключения не выдаётся за готовый канал опроса.
def open(self, name, console=False): # Прямой CDC либо общий FTDI сначала подтверждают службу и читают настройки без внешних операций.
self.close() # Прежняя сессия и её локальные клиенты завершаются до выбора нового физического канала.
if console: # Linux host для HDMI сохраняется, а существующий FTDI разделяется с CAN.
from .console_link_port import ConsoleLink # Общий менеджер является единственным владельцем COM для обеих страниц.
channel=ConsoleLink.acquire(name,502,self); self.serial=channel; self.tcp_transport=True; channel.readyRead.connect(self._receive); channel.opened.connect(self.read_configuration); channel.errorOccurred.connect(lambda error:self.fail(channel.errorString()) if not self.closing else None) # Чтение конфигурации начинается только после OPEN стандартного Modbus TCP listener той же службы платы.
self.state.emit('Общий USB FTDI: подключение к Modbus-службе Zynq…'); return True # Ожидающую сессию можно отменить, не закрывая соседний CAN.
self.serial.setPortName(name); self.serial.setBaudRate(115200); self.serial.setDataBits(QSerialPort.DataBits.Data8); self.serial.setParity(QSerialPort.Parity.NoParity); self.serial.setStopBits(QSerialPort.StopBits.OneStop); self.serial.setFlowControl(QSerialPort.FlowControl.NoFlowControl) # Параметры прямого CDC не зависят от физической линии RS485.
if not self.serial.open(QIODevice.OpenModeFlag.ReadWrite): self.state.emit(self.serial.errorString()); return False # Занятый CAN-монитором COM не перехватывается и не закрывается принудительно.
self.serial.clear(); self.serial.setDataTerminalReady(True); self.state.emit('USB открыт; чтение настроек Zynq…'); self.read_configuration(); return True # DTR объявляет gadget готовность host к передаче данных.
def close(self): # Вызывается при закрытии общей вкладки, ошибке и ручном отключении.
self.closing = True; self.timer.stop(); self.server.close(); sockets = list(self.buffers); self.buffers.clear(); self.queue.clear(); self.pending = None; self.received.clear(); self.ready = False # Ни один старый запрос не переносится в следующую сессию USB.
for socket in sockets: socket.abort(); socket.deleteLater() # Loopback-клиенты немедленно получают потерю соединения и прекращают опрос.
if self.serial.isOpen(): self.serial.close() # Физический COM освобождается для других приложений.
if self.tcp_transport: self.serial.deleteLater(); self.serial=self.direct; self.tcp_transport=False # Только логический FTDI канал возвращается к прямому порту; тестовый либо другой прямой I/O сохраняет свой контракт.
self.closing = False; self.connected.emit(False) # UI отключает рабочие действия после очистки транспорта.
def fail(self, message): # Ошибка не провоцирует повтор внешней команды записи.
if self.closing: return # Синхронные serial error callbacks во время close не вызывают рекурсию.
self.close(); self.state.emit(message) # Пользователь явно восстанавливает соединение после неопределённого результата записи.
def _invalid_response(self,reason): # Диагностические bytes сохраняются до закрытия сессии, очищающего буфер.
preview=bytes(self.received[:32]).hex(' ').upper(); suffix=' …' if len(self.received)>32 else ''; message=f'{self.serial.portName()}: {reason}. Получено {len(self.received)} байт; HEX: {preview}{suffix}.' # Предел 32 байта сохраняет полезное начало ответа без неограниченного роста сообщения GUI.
if not self.ready and not self.tcp_transport: message+=' Для штатного FTDI включите «Linux-консоль FTDI»; прямой CDC/UART требует выделенной службы шлюза.' # Единый образ не отправляет бинарный RTU в консоль Linux при правильно выбранном общем транспорте.
self.fail(message) # Строгая проверка ответа по-прежнему закрывает сессию без повтора внешней операции.
def _serial_error(self, error): # Ошибки NoError и ожидания не закрывают исправный endpoint.
if error not in (QSerialPort.SerialPortError.NoError, QSerialPort.SerialPortError.TimeoutError) and self.serial.isOpen(): self.fail(self.serial.errorString()) # Отсоединение USB прекращает и настройки, и встроенный опрос.
def read_configuration(self): self._enqueue(self.codec.control(), None) # Чтение настроек не посылает запросы внешним устройствам.
def apply_configuration(self, config): self._enqueue(self.codec.control(config), None, config) # Полная таблица отправляется одной атомарной командой управления.
def _enqueue(self, request, socket, candidate=None): # Один незавершённый RTU запрос сохраняет однозначность ответа без Transaction ID.
if not self.serial.isOpen(): self.state.emit('Сначала подключите USB Zynq'); return # Закрытый COM не накапливает команды на будущий запуск.
if len(self.queue) >= 32: self.fail('Переполнена очередь Modbus; уменьшите частоту опроса'); return # Ограничивает память при слишком быстром loopback-клиенте.
self.queue.append((request, socket, candidate)); self._next() # Кандидат настроек нужен только для достаточного дедлайна применения.
def _next(self): # Следующая команда отправляется только после полного проверенного ответа предыдущей.
if self.pending is not None or not self.queue: return # Исключает перемешивание данных опроса с чтением конфигурации.
self.pending = self.queue.popleft(); request, socket, candidate = self.pending # Неподтверждённая запись остаётся ровно одной записью в очереди.
if socket is not None and socket not in self.buffers: self.pending = None; self._next(); return # Запрос закрытого клиента удаляется до обращения к внешней шине.
wire = self.codec.tcp_reply(request,bytes(request.pdu[:request.length])) if self.tcp_transport else self.codec.rtu(request); self.received.clear() # Общий FTDI передаёт MBAP, прямой CDC — RTU; оба формата собирает один native-кодек.
if not wire or self.serial.write(wire) != len(wire): self.fail('Не удалось передать запрос по USB'); return # Ошибка локальной очереди записи не приводит к автоматическому повтору.
timeout = max(self.config.timeout_ms, candidate.timeout_ms if candidate is not None else 0); self.timer.start(max(5000, timeout + 1500)) # Учитывает внешний тайм-аут платы, USB и небольшую очередь обслуживания.
def _receive(self): # Границы USB transfer не считаются границами Modbus RTU ADU.
chunk = bytes(self.serial.readAll()) # Читает уже доступные байты без ожидания.
if self.pending is None: return # Посторонние байты вне транзакции не становятся ответом будущего запроса.
self.received.extend(chunk); size = (self.codec.lib.smg_tcp_size if self.tcp_transport else self.codec.lib.smg_rtu_response_size)(bytes(self.received), len(self.received)) # Границы MBAP либо RTU, включая частную FC65, вычисляет только C-ядро.
if size < 0 or len(self.received) > (260 if self.tcp_transport else 256) or (size > 0 and len(self.received) > size): self._invalid_response('Неверный формат или длина ответа USB Zynq'); return # Сохраняет диагностические bytes установленной панели при строгих native границах RTU/MBAP.
if not size or len(self.received) < size: return # Незавершённый ADU остаётся в ограниченном буфере до следующего readyRead.
request, socket, candidate = self.pending # Ответ принадлежит единственному исходящему запросу.
try: pdu = self.codec.accept(request, self.received, tcp=self.tcp_transport) # Native проверка CRC/MBAP и корреляции обязательна для обоих вариантов физического USB.
except ValueError as error: self._invalid_response(str(error)); return # Ошибки CRC, адреса и MBAP получают ту же диагностику физического канала и фактических bytes.
self.timer.stop(); self.pending = None; self.received.clear() # Освобождает транзакцию только после проверки полного ответа.
if socket is None: # Управление шлюзом обрабатывается отдельно от обычных регистров устройств.
try: config = self.codec.read_config(pdu) # Отказ ARM при настройке UART остаётся видимой ошибкой применения.
except ValueError as error: self.state.emit(str(error)); self._next(); return # Прежняя подтверждённая конфигурация остаётся действующей.
self.config = config # Тайм-аут и маршруты подтверждены работающей службой платы.
if not self.ready: # Локальный endpoint открывается после handshake с правильной службой, а не после простого открытия COM.
if not self.server.listen(QHostAddress.SpecialAddress.LocalHost, 0): self.fail(self.server.errorString()); return # ОС выделяет свободный порт без конфликтов с другими приложениями.
self.ready = True; self.connected.emit(True) # Встроенная панель может подключиться к готовому каналу опроса.
self.configuration.emit(config); self.state.emit('USB Zynq подключён; настройки подтверждены платой') # UI не выдаёт локальный черновик за применённую таблицу.
elif socket in self.buffers: socket.write(self.codec.tcp_reply(request, pdu)) # Исходные Transaction ID и Unit ID возвращаются клиенту через C builder.
self._next() # Продолжает очередь после доставки ответа его владельцу.
def _accept(self): # Loopback принимает только локальные Modbus-клиенты процесса или ПК.
while self.server.hasPendingConnections(): # Несколько ожидающих подключений обрабатываются за одно уведомление Qt.
socket = self.server.nextPendingConnection() # QObject TCP-сокета остаётся дочерним серверу.
if len(self.buffers) >= 8: socket.abort(); socket.deleteLater(); continue # Лимит совпадает с числом TCP-клиентов ARM-службы.
self.buffers[socket] = bytearray(); socket.readyRead.connect(lambda s=socket: self._tcp_read(s)); socket.disconnected.connect(lambda s=socket: self._tcp_closed(s)) # Каждая сессия имеет свой MBAP stream buffer.
def _tcp_closed(self, socket): self.buffers.pop(socket, None); socket.deleteLater() # Запросы закрытого клиента удаляются из очереди перед отправкой по USB.
def _tcp_read(self, socket): # Qt Modbus использует существующий TCP backend, сохраняя единый UI опроса.
if socket not in self.buffers: return # Позднее queued notification после disconnect не обращается к удалённому буферу.
buffer = self.buffers[socket]; buffer.extend(bytes(socket.readAll())) # Склеенные и частичные TCP ADU сохраняются без потери байтов.
if len(buffer) > 1040: socket.abort(); return # Четыре максимальных ADU ограничивают память недоверенного локального клиента.
while buffer: # Один read может содержать несколько законченных Modbus-запросов.
size = self.codec.lib.smg_tcp_size(bytes(buffer), len(buffer)) # Границы MBAP проверяет тот же код, что ARM-служба.
if size < 0: socket.abort(); return # Неизвестный Protocol ID и длина прекращают только эту TCP-сессию.
if not size or len(buffer) < size: return # Неполный заголовок не считается ошибкой и ждёт следующий пакет.
request = self.codec.decode_tcp(buffer[:size]); del buffer[:size]; error = self.codec.lib.smg_check(ct.byref(request)) # Неподдерживаемая функция либо ошибочный размер PDU не превращаются в неклассифицируемый USB stream.
if error: socket.write(self.codec.tcp_reply(request,self.codec.exception(request,error))) # Возвращает стандартное исключение без какой-либо внешней операции.
else: self._enqueue(request, socket) # Внешний запрос остаётся единственным, даже когда входной TCP разбит или склеен.
+51
View File
@@ -0,0 +1,51 @@
"""Эталонные обмены SCI ROM и TI Flash Kernel A без настоящей платы.""" # Проверки не открывают COM и не вызывают внешнюю программу C2Prog.
import struct # Формирует независимые тестовые SCI8-векторы с явно заданными словами и адресами.
import pytest # Проверяет отказ при повреждениях и граничные размеры порций ядра TI.
from protocan.c2000_sci import Session, decode, validate, ulitco_enter # Испытывает настоящий ctypes-порт общего C99-ядра.
def sci8(address, words): # Готовит минимальную адресную таблицу для эталонных данных теста.
pointer = struct.pack("<HH", address >> 16, address & 65535) # SCI8 хранит старшее слово адреса перед младшим.
return b"\xaa\x08" + bytes(16) + pointer + struct.pack("<H", len(words)) + pointer + struct.pack("<" + "H" * len(words), *words) + bytes(2) # Вектор содержит ключ, резерв, entry, одну секцию и терминатор.
@pytest.mark.parametrize("words", [1, 1024, 1025, 2048, 2050]) # Границы проверяют обычную порцию, остаток и дополнительный нулевой ACK официального TI kernel.
def test_complete_echo_and_checksum_transcript(words): # Фактические ответы подтверждают порядок и неизменность всего приложения.
kernel, image = sci8(0x9000, [0x1234, 0x5678]), sci8(0x328000, [0xA55A] * words) # RAM-ядро и Flash занимают разные разрешённые области.
session, received, pauses, zero_acks = Session(kernel, image), bytearray(), [], 0 # Наблюдения не используют внутреннюю структуру C-контекста.
for _ in range(len(kernel) + words + 100): # Конечный предел выявляет зацикливание автомата.
state, tx, expected, delay = session.next() # Проверяет настоящий сформированный C-пакет.
if state == 0: break # Успех допустим лишь после отправки нулевого терминатора приложения.
if state == 2: pauses.append(delay); continue # Внешний транспорт выполняет только заданную алгоритмом задержку.
if expected == 1: session.accept(tx) # ROM и autobaud должны вернуть точное эхо отправленного октета.
else: # Kernel A подтверждает сумму октетов после erase/program, без побайтового эха приложения.
received.extend(tx) # Снимок подтверждает отсутствие пропущенных, повторённых либо изменённых октетов SCI8.
if expected: session.accept(struct.pack("<H", sum(tx) & 65535)) # Независимый эталон суммы следует опубликованному исходнику TI SendCheckSum.
if not tx: zero_acks += 1 # Пустой остаток TI подтверждается отдельным нулевым словом.
else: pytest.fail("Автомат SCI не завершился") # Отсутствие терминального перехода считается ошибкой протокола.
assert bytes(received) == image # MCU получил ровно исходный Flash SCI8 без переименования или подмены содержимого.
assert pauses == [3000, 3000] and session.progress == 100 # Порядок ожиданий совпадает с TI f05_DownloadImage.
assert zero_acks == int(words > 1024 and words % 1024 == 0) # Обрабатывает особенность исходного TI Kernel A на кратной порции.
def test_corrupt_echo_and_checksum_stop_transfer(): # Повреждённое подтверждение не должно разрешать следующий пакет.
kernel, image = sci8(0x9000, [1]), sci8(0x328000, [2]) # Минимальные векторы отделяют проверку ответа от больших файлов.
session = Session(kernel, image); session.next() # Первый пакет требует эхо ASCII A.
with pytest.raises(ValueError, match="не совпали"): session.accept(b"B") # Чужой байт autobaud не принимается как связь с ROM.
session = Session(kernel, image) # Независимый сеанс проверяет повреждение двухоктетной суммы Flash.
while True: # Доходит до первой операции erase/header без использования внутренних фаз автомата.
state, tx, expected, _ = session.next() # Паузы не требуют аппаратного ответа.
if state == 2: continue # Номер стадии остаётся внутри C-ядра.
if expected == 1: session.accept(tx) # Корректный ROM позволяет загрузить RAM-ядро.
else: break # Первый двухоктетный ответ должен подтвердить заголовок приложения.
with pytest.raises(ValueError, match="не совпали"): session.accept(b"\xff\xff") # Неверная сумма не разрешает программирование следующей секции.
def test_ascii_decode_memory_ranges_and_monitor_command(): # Проверяет переносимость форматов и аппаратные границы до открытия порта.
image = sci8(0x328000, [0x1234]) # Обычный Flash-вектор не пересекает CSM.
ascii_image = b"\x02\n" + " ".join(f"{value:02X}" for value in image).encode("ascii") + b"\n\x03\n" # Формат соответствует выводу TI hex2000 -boot -sci8 -a.
assert decode(ascii_image) == image and validate(image) == (0x328000, 4) # Сектор C имеет маску 0x0004 в TI Flash API.
with pytest.raises(ValueError): decode(ascii_image[:-2]) # Усечённый ETX не должен оставить частично декодированное ядро.
with pytest.raises(ValueError): validate(sci8(0x33FFF8, [0x1234])) # Область CSM-паролей запрещена независимо от имени файла.
with pytest.raises(ValueError): validate(sci8(0x8000, [0x1234])) # RAM-образ не может стать Flash-приложением из-за выбранного loader.
assert ulitco_enter() == bytes([247, 213, 255, 93, 0, 0, 1, 0, 93, 1]) # Эталон совпадает с десятиоктетным Monitor-запросом FLASH_NUM=93 текущей прошивки UlitCo.
+126
View File
@@ -1,5 +1,131 @@
# Подключение precompile и postcompile из templates
## Параметры прошивки CAN и RS из JSON
Настройка CCS/Keil в SETGUI теперь проверяет Python до изменения проекта.
Общие BAT вызывают `python-runtime.ps1`: он находит Python 3.8+ в установленном
окружении либо готовит проверенный переносимый runtime в
`%LOCALAPPDATA%/SET/FirmwareBuild`. Ручная настройка PATH не требуется.
Для Windows 10/11 x64 используется закреплённый Python 3.13.12, для Windows 7
и x86 — Python 3.8.10. ZIP загружается с python.org с проверкой SHA-256;
повторные сборки используют готовую локальную копию. При отсутствии доступа
к сети можно заранее установить Python либо задать полный путь `FIRMWARE_PYTHON`.
Для **штатного SCI-загрузчика UlitCo / C2Prog** выберите в настройке проекта
маршрут `f28335-sci`. Он автоматически подключает общую реализацию из
`c/firmware-info/tools/c2000_sci_build.py`; отдельный проектный `build_flash.py`
не требуется. Пример блока:
```json
"flashing": {
"default_loader": "f28335-sci",
"profiles": {
"f28335-sci": {
"baudrate": 115200,
"oscillator_hz": 30000000,
"enter_monitor": false,
"timeout_ms": 3000
}
}
}
```
Укажите фактическую частоту кварца: поддержаны 20, 25 и 30 МГц. Она определяет
времена Flash API и сохраняется вместе с RAM-ядром. Установленный C2000Ware с
`device_support/f2833x/examples/f28335_flash_kernel` определяется автоматически
в `C:/ti` либо по `C2000WARE_ROOT`. Эта зависимость проверяется при применении
настройки проекта. TI compiler берётся из `${CG_TOOL_ROOT}` текущей CCS.
Postbuild проверяет Flash LOAD-секции исходного OUT и выпускает
`*_flash_rs.bin`, `*_flash.out`, `f28335_sci_kernel.bin`, manifest, SHA-256
и уведомление о лицензии TI. Ядро собирается из штатных исходников SDK под
частоту платы и маску секторов именно текущего приложения. Основной manifest
в `latest_build.json` указывает сразу на Flash SCI BIN; папка `eeprom` для
этого маршрута не создаётся.
В SETGUI маршрут работает внутри общей вкладки прошивки: первичный загрузчик
принимает RAM-ядро TI с побайтовым эхом, затем Kernel A стирает выбранные сектора
и принимает порции Flash с подтверждением checksum после Flash API. Внешний
C2Prog не запускается. Тайм-аут, несовпадение эха/суммы и отмена завершают
операцию; readback-проверка памяти этим протоколом Kernel A не заявляется.
Ядро автоматически выбирается из manifest и проверяется по SHA-256.
`enter_monitor=true` включает штатную команду UlitCo: запись `1` в параметр
Monitor №93 перед autobaud. Действующая прошивка разрешает такой переход только
в остановленном состоянии. При `false` предварительно переведите плату в
первичный SCI-загрузчик обычным способом. Это отдельный маршрут относительно
`f28335-flash-rs`, который использует BIOS ПМ35 и защищённое приложение FAB1.
В проектном `firmware_identity.json` можно хранить блок `flashing` вместе с
версией. Keil и CCS проверяют его до увеличения номера сборки и переносят в
`generated/firmware_identity.json`, каждый `*.manifest.json` и архив выпуска.
Параметры описывают **загрузку образа**; скорость работающей программы на MCU
по-прежнему задаётся её исходниками и настройками аппаратуры.
```json
"flashing": {
"default_loader": "set-can",
"profiles": {
"set-can": {"bitrate": 250000, "node_id": 13},
"set-rs485": {"baudrate": 115200}
}
}
```
`default_loader` — конкретный загрузчик, выбираемый при импорте сборки или
локального образа; его ключ должен присутствовать в `profiles`. Каждая запись
`profiles` задаёт независимые параметры соответствующего загрузчика. Значения
примеров нужно заменить параметрами своей платы. JSON не допускает комментарии;
назначение полей приведено ниже.
| Загрузчик | Поддерживаемые параметры |
|---|---|
| `set-can` | `bitrate`, `node_id`, `product_type`, `hardware_min`, `hardware_max`, `base_address` |
| `protocan-can` | Поля `set-can`, кроме `base_address`; дополнительно `device_type`, `session_id`, `memory` |
| `set-rs485`, `stm32-uart` | `baudrate`, `base_address` |
| `f28335-eeprom`, `f28335-flash-rs` | `baudrate`, `controller`, `timeout_ms` |
| `stm32-usb` | `base_address` |
| `manual`, `f28335-jtag` | Пустой объект `{}` |
| Поле | Единицы и ограничения |
|---|---|
| `bitrate` | Целое число бит/с, `1..1000000`; верхняя граница Classic CAN. Например, `250000` — 250 кбит/с. Адаптер также должен поддерживать выбранную скорость. |
| `baudrate` | Целое число бод, `1..2147483647`; фактическую поддержку проверяет COM-драйвер. Например, `115200`. |
| `node_id` | Адрес SETP `0..255`; для ProtoCAN — поле Device `0..15`. |
| `device_type`, `session_id` | Поля существующего ProtoCAN-загрузчика: Type `0..7`, Session ID `1..255`. |
| `product_type` | Код совместимости `0..65535`; например, `358` соответствует `0x0166`, а `10291` — `0x2833`. |
| `hardware_min`, `hardware_max` | Включительный диапазон аппаратных ревизий `0..255`; min не больше max. |
| `base_address` | Целочисленный адрес приложения в **байтах**, `0..4294967295`. Например, `134230016` соответствует `0x08003000`; это адрес примера, а не общий адрес всех STM32. Для HEX должен совпадать с адресом самого образа. |
| `controller` | Независимый адрес контроллера RS-протокола ПМ35, `0..255`. |
| `timeout_ms` | Дополнительный тайм-аут RS-панели ПМ35 в миллисекундах, `50..10000`. |
| `memory` | Выбор назначения ProtoCAN: `eeprom`, `flash` или `stm32`. |
Формат UART определяется протоколом: SET RS использует 8N1, STM32 ROM Boot —
8E1. COM-порт, экземпляр CAN-адаптера и его соединение выбираются на компьютере
оператора, поскольку они не являются свойствами образа прошивки.
Обновлённый SETGUI выбирает загрузчик и заполняет форму из manifest при импорте
postbuild и выборе опубликованного комплекта. Общая вкладка прошивки также
подхватывает настройки при обычном выборе локального BIN/HEX, если рядом лежит
соответствующий `имя.bin.manifest.json` или `имя.hex.manifest.json`; перед
применением проверяются имя, размер и SHA-256 файла. Маршрут выбранной записи
каталога использует свой профиль, даже если `default_loader` задаёт другой.
При переключении общей формы между CAN и RS применяется профиль нового режима.
Локальный выбор BIN в панели F28335 подхватывает профиль выбранного RS-загрузчика
после штатной проверки образа; JTAG сохраняет отдельное подключение.
Скорость уже открытого канала применяется после его отключения. Запись платы
запускается отдельной штатной кнопкой.
Для отдельных CAN BIN и Flash RS BIN C2000 назначение из паспорта артефакта
имеет приоритет над `default_loader` основного EEPROM-образа. Профили при этом
остаются общими для комплекта. Старые конфигурации без `flashing` сохраняют
прежнее поведение; существующие проектные JSON автоматически не заменяются.
Полные примеры: [Keil](keil/firmware_identity.example.json) и
[CCS](ccs/firmware_identity.example.json). Поддержка чтения профилей должна быть
включена в сборку SETGUI вместе с обновлённым модулем
`set_devices.firmware_flashing` из templates.
Для проектов ПЛИС используйте [шаблон Xilinx ISE, Vivado и Altera/Intel Quartus](fpga/README.md):
общий сохраняемый итератор, паспорт на Verilog/VHDL, полная сборка `.bit`/`.sof`,
manifest, SHA-256 и архив выпуска.
@@ -2,5 +2,36 @@
"product": "MY_CONTROLLER",
"target": "TMS320F28335",
"version": [0, 1, 0],
"increment_version": true
"increment_version": true,
"flashing": {
"default_loader": "f28335-eeprom",
"profiles": {
"f28335-eeprom": {
"baudrate": 115200,
"controller": 16,
"timeout_ms": 500
},
"f28335-flash-rs": {
"baudrate": 115200,
"controller": 16,
"timeout_ms": 500
},
"f28335-sci": {
"baudrate": 115200,
"oscillator_hz": 30000000,
"enter_monitor": false,
"timeout_ms": 3000
},
"protocan-can": {
"bitrate": 250000,
"device_type": 1,
"node_id": 8,
"session_id": 1,
"product_type": 10291,
"hardware_min": 1,
"hardware_max": 1,
"memory": "eeprom"
}
}
}
}
+4 -2
View File
@@ -19,11 +19,13 @@ if "%~6"=="" goto usage
if not "%~8"=="" goto usage
if "%~7"=="" goto identity_local
rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI.
python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" --export "%~f7"
rem Тот же общий runtime обслуживает postbuild и экспорт текущего проверенного выпуска.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6" --export "%~f7"
exit /b %errorlevel%
:identity_local
rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI.
python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6"
rem Локальный выпуск использует ту же проверенную установку Python, что prebuild.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" build --compiler "%~f2" --build-dir "%~f3" --artifact "%~4" --metadata "%~f5" --output "%~f6"
exit /b %errorlevel%
:usage
echo Usage: postcompile.bat --identity "compiler-root" "build-dir" "artifact-name" "generated-metadata.json" "output-dir" ["export-dir"] 1>&2
+2 -1
View File
@@ -24,7 +24,8 @@ if not exist "%~f3" (
echo [firmware-build] Edit product, target and version in this file, then rebuild.
)
rem Общая подготовка и проверка выполняются в templates; сетевую публикацию выполняет Публикатор SETGUI.
python "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" prepare --project "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4"
rem Общая обвязка находит Python 3.8+ или готовит локальный runtime до резервирования номера сборки.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\c2000_build.py" prepare --project "%~f2" --templates "%~dp0..\..\.." --config "%~f3" --output "%~f4"
exit /b %errorlevel%
:usage
echo Usage: precompile.bat --identity "project-root" "firmware_identity.json" "generated-dir" 1>&2
@@ -2,5 +2,22 @@
"product": "MY_CONTROLLER",
"target": "STM32F407",
"version": [0, 1, 0],
"increment_version": true
"increment_version": true,
"flashing": {
"default_loader": "set-can",
"profiles": {
"set-can": {
"bitrate": 250000,
"node_id": 13,
"product_type": 358,
"hardware_min": 1,
"hardware_max": 1,
"base_address": 134230016
},
"set-rs485": {
"baudrate": 115200,
"base_address": 134230016
}
}
}
}
+8 -4
View File
@@ -18,15 +18,19 @@ if /i not "%~x2"==".hex" if /i not "%~x2"==".bin" (
echo [firmware-build] Expected a ready HEX or BIN image: "%~2" 1>&2
exit /b 2
)
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" seal --image "%~f2" --metadata "%~f3"
rem Печать паспорта выполняется найденным или автоматически подготовленным Python.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" seal --image "%~f2" --metadata "%~f3"
if errorlevel 1 exit /b %errorlevel%
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" verify --image "%~f2" --manifest "%~f2.manifest.json"
rem Проверка хеша использует общий runtime вместо случайного python.exe в PATH.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" verify --image "%~f2" --manifest "%~f2.manifest.json"
if errorlevel 1 exit /b %errorlevel%
if "%~4"=="" goto archive_local
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3" --export "%~f4"
rem Экспорт архива получает те же аргументы и найденный интерпретатор, что остальные стадии.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3" --export "%~f4"
goto archive_done
:archive_local
python "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3"
rem Архивирование без дополнительной папки также не зависит от глобальной настройки Python.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0..\python-runtime.ps1" "%~dp0..\..\..\c\firmware-info\tools\identity.py" archive --image "%~f2" --metadata "%~f3"
:archive_done
if errorlevel 1 exit /b %errorlevel%
echo [firmware-build] Image: "%~f2"

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