Compare commits
14
Commits
zynq-unified
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28fe22439c | ||
|
|
26f9d62896 | ||
|
|
a197437985 | ||
|
|
4d101302be | ||
|
|
ae018a1347 | ||
|
|
80d8fe1794 | ||
|
|
d057df7698 | ||
|
|
f9d369d032 | ||
|
|
07929c880a | ||
|
|
4f7f6cdb87 | ||
|
|
1fc88684d5 | ||
|
|
fa5ea166a9 | ||
|
|
062f064c85 | ||
|
|
7b1a16d6e4 |
@@ -4,3 +4,8 @@
|
||||
|
||||
*.pdf binary
|
||||
*.png binary
|
||||
|
||||
# CMD.exe требует CRLF в BAT, иначе после переноса Git часть команд разбирается неверно.
|
||||
*.bat text eol=crlf
|
||||
# Командные файлы CMD выполняются тем же интерпретатором Windows и требуют CRLF.
|
||||
*.cmd text eol=crlf
|
||||
|
||||
@@ -27,3 +27,6 @@ Desktop.ini
|
||||
|
||||
# Generated native library (built before packaging)
|
||||
python/protocan/native/
|
||||
|
||||
# Кэш pytest создаётся проверками автоматически и не входит в исходники общей библиотеки.
|
||||
.pytest_cache/
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
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() # Завершает платформенную часть библиотеки.
|
||||
@@ -0,0 +1,75 @@
|
||||
# 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 платы.
|
||||
|
||||
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 не обновляет уже собранную прошивку автоматически.
|
||||
@@ -0,0 +1,24 @@
|
||||
#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
@@ -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 после сброса.
|
||||
@@ -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. */
|
||||
@@ -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
@@ -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) # Самостоятельный запуск выводит результаты модели без утверждений о проверке платы.
|
||||
@@ -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
@@ -39,6 +39,22 @@ class IdentityTests(unittest.TestCase):
|
||||
self.assertEqual(self.prepare([clean,dirty])["build_id"], "abcdef0+")
|
||||
self.assertEqual(self.prepare([dirty,clean])["build_id"], "abcdef0+")
|
||||
|
||||
def test_project_and_repository_survive_sealing(self): # Проверяет сведения, нужные Публикатору после переноса сборки на другой компьютер.
|
||||
for project_name in (None, "Бальзам — ПМ35"): # Поддерживает прежние конфигурации и явно заданное русское название.
|
||||
settings = {"product": "PM35", "target": "TMS320F28335", "version": [1, 2, 3]} # Код изделия сохраняет техническое назначение независимо от названия проекта.
|
||||
if project_name is not None: # Отсутствующее поле должно использовать совместимое значение по умолчанию.
|
||||
settings["project"] = project_name # Название из конфигурации переносится в паспорт без сокращений.
|
||||
self.config.write_text(json.dumps(settings), encoding="utf-8") # Конфигурация сохраняется в той же кодировке, что и проект пользователя.
|
||||
clean = {"commit": "a" * 40, "dirty": False} # Источник и templates остаются известными чистыми состояниями.
|
||||
metadata = self.prepare([dict(clean), dict(clean)]) # Генерирует настоящий precompile JSON и заголовок без запуска компилятора.
|
||||
self.assertEqual(metadata["project"], project_name or "PM35") # Старый JSON получает код изделия, новый — заданное название.
|
||||
self.assertEqual(metadata["source"]["repository"], str(self.root.resolve())) # Абсолютный путь сохраняется в состоянии исходного проекта.
|
||||
image = self.root / "controller.bin" # Отдельный образ позволяет проверить последующий postbuild manifest.
|
||||
image.write_bytes(b"firmware fixture") # Небольшой файл достаточен для проверки сохранения полей и SHA-256.
|
||||
sealed = identity.seal(image, self.root / "firmware_identity.json") # Штатный postbuild дополняет сохранённое происхождение без чтения нового HEAD.
|
||||
self.assertEqual(sealed["project"], metadata["project"]) # Название доходит до того manifest, который импортирует GUI.
|
||||
self.assertEqual(sealed["source"], metadata["source"]) # Путь, полный коммит и признак dirty переживают запечатывание образа.
|
||||
|
||||
def test_missing_git_is_explicit(self):
|
||||
unknown = {"commit":None,"dirty":None}
|
||||
self.assertEqual(self.prepare([unknown,unknown])["build_id"], "NOGIT000")
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Проверки CCS/Keil с интерфейсами pathlib и shutil, доступными в Python 3.7.""" # Воспроизводит ограничения старого Python без установки другого интерпретатора.
|
||||
import importlib.util # Загружает проверяемые инструменты именно из этой библиотеки templates.
|
||||
import json # Проверяет сохранённый счётчик и фактический паспорт компиляции.
|
||||
from pathlib import Path # Моделирует прежнюю сигнатуру удаления временных файлов.
|
||||
import shutil # Моделирует прежнюю сигнатуру копирования каталогов.
|
||||
import tempfile # Ограничивает запись изолированным каталогом теста.
|
||||
import unittest # Проверки запускаются стандартным Python без сторонних зависимостей.
|
||||
from unittest.mock import patch # Подменяет только несовместимые сигнатуры стандартной библиотеки.
|
||||
|
||||
|
||||
def load_tool(name): # Импортирует одну точку входа без запуска CLI.
|
||||
spec = importlib.util.spec_from_file_location("compatibility_" + name, Path(__file__).resolve().parents[1] / "tools" / (name + ".py")) # Выбор не зависит от PATH компьютера сборки.
|
||||
module = importlib.util.module_from_spec(spec) # Отдельный экземпляр сохраняет изоляцию других тестов.
|
||||
spec.loader.exec_module(module) # Загружает публичные функции подготовки и копирования.
|
||||
return module # Тесты вызывают те же функции, что BAT-обвязки двух IDE.
|
||||
|
||||
|
||||
identity, c2000 = load_tool("identity"), load_tool("c2000_build") # Обе цепочки используют общий формат паспорта.
|
||||
original_unlink, original_copytree = Path.unlink, shutil.copytree # Сохраняет настоящие реализации перед ограничением их сигнатур.
|
||||
|
||||
|
||||
def unlink37(path): # Python 3.7 не допускает аргумент missing_ok.
|
||||
return original_unlink(path) # Реальное удаление сохраняет настоящие ошибки доступа и отсутствующего файла.
|
||||
|
||||
|
||||
def copytree37(source, destination, symlinks=False, ignore=None, copy_function=shutil.copy2, ignore_dangling_symlinks=False): # Сигнатура Python 3.7 не допускает dirs_exist_ok.
|
||||
return original_copytree(source, destination, symlinks, ignore, copy_function, ignore_dangling_symlinks) # Старый интерфейс продолжает копировать обычные новые каталоги.
|
||||
|
||||
|
||||
class Python37CompatibilityTests(unittest.TestCase): # Проверяет успешный prebuild и повторный экспорт вместо одного синтаксического анализа.
|
||||
def setUp(self): # Каждый сценарий начинает с отдельного проекта со включённым итератором версии.
|
||||
temporary = tempfile.TemporaryDirectory(prefix="firmware python37 ") # Пробелы в пути соответствуют обычным пользовательским библиотекам.
|
||||
self.addCleanup(temporary.cleanup) # Очистка выполняется после снятия ограничений стандартной библиотеки.
|
||||
self.root = Path(temporary.name) # Рабочие проекты CCS/Keil не изменяются.
|
||||
self.config = self.root / "firmware_identity.json" # Конфигурация принадлежит только временной проверке.
|
||||
self.config.write_text(json.dumps({"product": "TEST_BOARD", "target": "TMS320F2812", "version": [1, 2, 3], "increment_version": True}), encoding="utf-8") # Этот режим воспроизводит исходную ошибку unlink после увеличения версии.
|
||||
|
||||
def test_keil_and_ccs_prepare_generate_headers_with_old_unlink(self): # Обе обвязки должны завершить генерацию при старой сигнатуре pathlib.
|
||||
with patch.object(Path, "unlink", unlink37): # Любой оставшийся missing_ok вызовет тот же TypeError, что в журнале пользователя.
|
||||
keil = identity.prepare(self.root, self.root, self.config, self.root / "Generated/target_0") # Keil напрямую вызывает общий prepare.
|
||||
ccs = c2000.prepare(self.root, Path(__file__).resolve().parents[3], self.root / "STEND40_PCH_1/generated", config=self.config) # CCS загружает генератор из полной проверяемой библиотеки templates.
|
||||
for relative, expected in (("Generated/target_0", [1, 2, 4]), ("STEND40_PCH_1/generated", [1, 2, 5])): # Две попытки резервируют разные номера без пропуска после TypeError.
|
||||
output = self.root / relative # Проверяет фактический include-каталог каждой IDE.
|
||||
self.assertIn("#define FIRMWARE_VERSION_PATCH " + str(expected[2]) + "U", (output / "firmware_identity.h").read_text(encoding="utf-8")) # Заголовок существует и содержит зарезервированную версию.
|
||||
self.assertEqual(json.loads((output / "firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Паспорт совпадает с константами для компилятора.
|
||||
self.assertEqual((keil["version"], ccs["version"]), ([1, 2, 4], [1, 2, 5])) # Обе публичные функции возвращают успешный результат.
|
||||
|
||||
def test_archive_export_and_atomic_copy_support_old_file_apis(self): # Проверяет повторное сохранение комплекта после успешного prebuild.
|
||||
generated = self.root / "generated" # Метаданные сохраняются отдельно от исходного JSON.
|
||||
identity.prepare(self.root, self.root, self.config, generated) # Получает настоящий паспорт для проверки SHA-256 и архива.
|
||||
image = self.root / "application.bin" # Произвольные байты достаточны для общего Keil-контракта файловой целостности.
|
||||
image.write_bytes(b"firmware bytes") # Проверяет сохранение точных байтов при нескольких копированиях.
|
||||
with patch.object(Path, "unlink", unlink37), patch.object(shutil, "copytree", copytree37): # Воспроизводит оба ограничения Python 3.7 одновременно.
|
||||
for attempt in range(2): # Второй экспорт должен объединять уже существующие каталоги.
|
||||
result = identity.archive_build(image, generated / "firmware_identity.json", self.root / "archive", export=self.root / "export") # Печать папки соответствует настоящему postcompile Keil.
|
||||
folder = Path(result["directory"]) # Использует точный архив текущего вызова.
|
||||
(folder / "nested").mkdir(exist_ok=True) # Вложенный сопроводительный файл проверяет рекурсивное копирование.
|
||||
(folder / "nested/map.txt").write_text("map " + str(attempt), encoding="utf-8") # Повторный выпуск обновляет существующий файл.
|
||||
identity.copy_tree(folder, self.root / "ccs-output", copy_function=c2000.atomic_copy) # C2000 сохраняет атомарную замену с прежним интерфейсом unlink.
|
||||
self.assertEqual((self.root / "ccs-output/nested/map.txt").read_text(encoding="utf-8"), "map 1") # Вложенные файлы обновились при повторном выпуске.
|
||||
self.assertEqual((Path(result["exported_directory"]) / image.name).read_bytes(), image.read_bytes()) # Экспорт содержит исходный образ без изменения байтов.
|
||||
self.assertFalse(list((self.root / "ccs-output").rglob(".firmware-*"))) # Успешная атомарная замена не оставляет незавершённых файлов.
|
||||
|
||||
|
||||
if __name__ == "__main__": # Позволяет запустить регрессию стандартным unittest на компьютере сборки.
|
||||
unittest.main() # Ненулевой результат выявляет несовместимость инструментов до настройки IDE.
|
||||
@@ -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.
|
||||
@@ -9,6 +9,7 @@ from __future__ import annotations
|
||||
import argparse
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import hashlib
|
||||
import importlib.util # Загружает общий контракт JSON без импорта протокольного runtime и без зависимости от установленного пакета.
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -18,6 +19,10 @@ from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile # Резервирует номер сборки через временный файл на том же диске, как общий итератор ветки 166.
|
||||
|
||||
_flashing_spec = importlib.util.spec_from_file_location("firmware_flashing_contract", Path(__file__).resolve().parents[3] / "python/set_devices/firmware_flashing.py") # Общий контракт переносится вместе с полным checkout templates.
|
||||
_flashing = importlib.util.module_from_spec(_flashing_spec) # Изолирует проверку настроек от GUI и системных транспортов.
|
||||
_flashing_spec.loader.exec_module(_flashing) # Использует ту же проверку полей, что SETGUI при чтении manifest.
|
||||
|
||||
|
||||
def git_state(repository: Path) -> dict:
|
||||
"""Record full commits, and flag untracked sources as well as tracked edits.
|
||||
@@ -66,6 +71,7 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
|
||||
for field in ("product", "target"):
|
||||
if not isinstance(settings.get(field), str) or not settings[field].strip():
|
||||
raise ValueError(f"missing {field}")
|
||||
flashing = _flashing.validate_flashing(settings.get("flashing", {})) # Проверяет скорости и адреса до резервирования следующего номера сборки.
|
||||
if settings.get("increment_version", False): # Прямые вызовы templates из CCS, Keil и CLI используют один и тот же сохраняемый итератор.
|
||||
version = settings["version"] = next_version(version) # Вычисляет номер для JSON, заголовка и метаданных только один раз.
|
||||
with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", newline="\n", dir=config.parent, prefix=".firmware-version-", delete=False) as stream: # Временный файл на том же диске позволяет атомарно заменить настройки без частичной записи.
|
||||
@@ -73,8 +79,13 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
|
||||
try: stream.write(json.dumps(settings, ensure_ascii=False, indent=2) + "\n") # Сохраняет новый номер и все дополнительные поля проекта в UTF-8.
|
||||
except BaseException: stream.close(); temporary.unlink(); raise # Ошибка записи оставляет исходный JSON целым и удаляет незавершённый файл.
|
||||
try: temporary.replace(config) # Занимает номер до дальнейших шагов, поэтому отказ генерации или компиляции не повторит его.
|
||||
finally: temporary.unlink(missing_ok=True) # Удаляет временный файл при ошибке замены; после успеха он уже переименован.
|
||||
finally: # Очистка должна работать и в Python 3.7, который ещё не принимает missing_ok.
|
||||
try: # После успешной замены временное имя уже отсутствует.
|
||||
temporary.unlink() # Удаляет только собственный незавершённый файл при отказе replace.
|
||||
except FileNotFoundError: # Отсутствие файла после успешного rename является штатным результатом.
|
||||
pass # Другие ошибки файловой системы продолжают останавливать подготовку.
|
||||
source, dependency = git_state(repository), git_state(templates)
|
||||
source = dict(source, repository=str(repository.resolve())) # Сохраняет абсолютный локальный путь исходного проекта для Публикатора без адресов remote и учётных данных.
|
||||
# A clean application built with a modified templates checkout is dirty too.
|
||||
known = source["commit"] is not None and dependency["commit"] is not None
|
||||
dirty = bool(source["dirty"] or dependency["dirty"])
|
||||
@@ -86,10 +97,11 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
|
||||
stamp = stamp.replace(microsecond=0)
|
||||
if not 2000 <= stamp.year <= 2199:
|
||||
raise ValueError("build year outside firmware-info v1 range")
|
||||
metadata = {"schema": 1, "product": settings["product"], "target": settings["target"],
|
||||
metadata = {"schema": 1, "project": settings.get("project") or settings["product"], "product": settings["product"], "target": settings["target"], # Необязательное название проекта переносится из конфигурации; старые проекты используют свой код изделия.
|
||||
"version": version, "build_id": build_id, "built_at_utc": stamp.isoformat(),
|
||||
"source": source, "templates": dependency,
|
||||
"identity_contract": 1, "identity_register_count": 12}
|
||||
if "flashing" in settings: metadata["flashing"] = flashing # Переносит все CAN/RS-профили в prebuild, manifest, архив и комплект Публикатора без изменения бинарного протокола паспорта.
|
||||
constants = dict(zip(("FIRMWARE_VERSION_MAJOR", "FIRMWARE_VERSION_MINOR", "FIRMWARE_VERSION_PATCH"), version))
|
||||
constants.update(dict(zip(("FI_YEAR", "FI_MONTH", "FI_DAY", "FI_HOUR", "FI_MINUTE", "FI_SECOND"),
|
||||
(stamp.year, stamp.month, stamp.day, stamp.hour, stamp.minute, stamp.second))))
|
||||
@@ -152,6 +164,19 @@ def build_folder_name(metadata: dict) -> str:
|
||||
return identity + "_" + stamp.strftime("%Y-%m-%d_%H-%M-%S_UTC+3")
|
||||
|
||||
|
||||
def copy_tree(source: Path, destination: Path, copy_function=shutil.copy2): # Объединяет архив с существующим каталогом без dirs_exist_ok, отсутствующего в Python 3.7.
|
||||
source, destination = Path(source), Path(destination) # Принимает пути обоих типов из общего экспорта и проверенной C2000-сборки.
|
||||
destination.mkdir(parents=True, exist_ok=True) # Повторный экспорт сохраняет соседние файлы, но не принимает обычный файл вместо папки.
|
||||
for entry in source.iterdir(): # Копирует только содержимое конкретного проверенного архива.
|
||||
target = destination / entry.name # Сохраняет исходные имена и вложенную структуру комплекта.
|
||||
if entry.is_dir(): # Вложенные каталоги требуют того же объединения при повторном выпуске.
|
||||
copy_tree(entry, target, copy_function) # Передаёт атомарное копирование C2000 на все уровни архива.
|
||||
else: # Каждый файл обрабатывается выбранным способом сохранения.
|
||||
copy_function(entry, target) # По умолчанию сохраняет метаданные copy2, а C2000 использует atomic_copy.
|
||||
shutil.copystat(source, destination) # Восстанавливает время и атрибуты каталога после записи дочерних файлов.
|
||||
return str(destination) # Сохраняет возвращаемый тип стандартного shutil.copytree.
|
||||
|
||||
|
||||
def export_build(folder: Path, export: Path, image_name: str) -> Path:
|
||||
"""Copy the current complete build folder, without scanning old archives."""
|
||||
folder, export = folder.resolve(), export.resolve()
|
||||
@@ -161,7 +186,7 @@ def export_build(folder: Path, export: Path, image_name: str) -> Path:
|
||||
verify(folder / image_name, folder / (image_name + ".manifest.json"))
|
||||
export.mkdir(parents=True, exist_ok=True)
|
||||
if destination != folder:
|
||||
shutil.copytree(folder, destination, dirs_exist_ok=True)
|
||||
copy_tree(folder, destination) # Повторное копирование архива совместимо с Python 3.7 без потери соседних выпусков.
|
||||
verify(destination / image_name, destination / (image_name + ".manifest.json"))
|
||||
print(f"[firmware-build] Exported folder: {destination}")
|
||||
return destination
|
||||
|
||||
@@ -10,6 +10,7 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
|
||||
# SET protocol v2: управление, телеметрия, CAN segmentation и firmware flow.
|
||||
set(SETPROTOCOL_V2_SOURCES
|
||||
src/c2000_sci.c # Общий автомат SCI Boot F28335 входит во все host и MCU цели.
|
||||
src/firmware_image.c
|
||||
src/altera_logic.c
|
||||
src/altera_stream.c
|
||||
@@ -28,6 +29,7 @@ set(SETPROTOCOL_V2_SOURCES
|
||||
|
||||
# Совместимые ProtoCAN/SETGUI v1 форматы переходного периода.
|
||||
set(SETPROTOCOL_LEGACY_SOURCES
|
||||
src/uksvep_can.c # Общий профиль STM32 UKSVEP: маршруты, payload и карта дискретов.
|
||||
src/balsam_can.c
|
||||
src/set_crc.c
|
||||
src/periph28335.c
|
||||
@@ -46,6 +48,7 @@ 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
|
||||
@@ -99,7 +102,13 @@ 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) # Проверяет тот же C99-модуль, что использует ctypes.
|
||||
add_test(NAME c2000_sci COMMAND test_c2000_sci) # Регистрирует контракт в обычном CTest.
|
||||
add_executable(test_deif_xdi tests/test_deif_xdi.c) # Прямые проверки C-контракта DEIF.
|
||||
target_link_libraries(test_deif_xdi PRIVATE setprotocol_static) # Используется единственное общее ядро.
|
||||
add_test(NAME deif_xdi COMMAND test_deif_xdi "${CMAKE_CURRENT_SOURCE_DIR}/tests/fixtures/deif-xdi-v1.tsv") # Векторы общие с JNI и Python.
|
||||
|
||||
@@ -131,3 +131,10 @@ 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, без настройки профиля.
|
||||
|
||||
## УКСВЭП STM32F103RC
|
||||
|
||||
Общий профиль `uksvep_can.h/.c` предоставляет карту дискретов и параметров,
|
||||
маршруты проекта «Ледокол», C-кодирование регистрового CAN и расчёт состояний
|
||||
для эмулятора SETGUI. Контракт ABI, единицы и проверки:
|
||||
[UKSVEP_CAN.md](docs/UKSVEP_CAN.md).
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# UKSVEP STM32F103RC: регистровый CAN
|
||||
|
||||
Профиль относится к `UKSVEP_ICE_22220.6`: `Core/Src/can.c`,
|
||||
`Core/Inc/package.h`, `Core/Src/message.c`, `Core/Src/lampa.c` и `main.h`.
|
||||
Он подключён в SETGUI как отдельный эмулятор проекта «Ледокол».
|
||||
|
||||
Classic CAN, extended 29-битный ID, DLC=8, 250000 бит/с.
|
||||
DeviceID 0…15 задают J0…J3; `Mode=DeviceID+1`.
|
||||
Данные ВЭП: `0x001CE000 | DeviceID`; команды ВЭП:
|
||||
`0x001CE020 | DeviceID`. Это регистровое приложение, а не ProtoCAN Boot Type5.
|
||||
Broadcast из старого приложения не выдаётся этим клиентом за адресный ответ.
|
||||
|
||||
Payload общий с историческим `balsam_can`: word2, word3,
|
||||
адрес/маска, word1, по два байта big endian. В заголовке биты15…13 задают
|
||||
присутствие слов, биты12…0 — начальный адрес. `uksvep_can` ограничивает
|
||||
банк R0…R127; отсутствующие слова не должны обнулять предыдущее состояние.
|
||||
Маршрутизация других Balsam-плат остаётся в их собственном профиле.
|
||||
|
||||
| Регистры | Назначение |
|
||||
|---|---|
|
||||
| 0 / 1 / 2 | Входы / неисправности / аварии |
|
||||
| 8 / 9 | Маски неисправностей / аварий |
|
||||
| 23 | Перемычки; младший полубайт соответствует DeviceID |
|
||||
| 48 | Sleep_time, секунды |
|
||||
| 100 | Brightness, 0…10 тактов ШИМ |
|
||||
| 101 / 102 | Cancount, быстрый / медленный период по 10 мс |
|
||||
| 125 / 126 / 127 | Версия протокола / Mode / команды |
|
||||
|
||||
Назначения входов, GPIO, команд и значения по умолчанию доступны через
|
||||
`uksvep_field_*`, `uksvep_input_name`, `uksvep_command_name`.
|
||||
`uksvep_evaluate_inputs` моделирует ReadEnteres: R0.7 никогда не является
|
||||
неисправностью/аварией; R0.8 всегда является неисправностью и становится
|
||||
аварией по Error_mask.8; R0.9 всегда является неисправностью и аварией.
|
||||
Вход R0.9/PB12 читается без инверсии, остальные назначенные
|
||||
сигналы собираются в соответствии с физической картой STM32-проекта.
|
||||
|
||||
## Слои
|
||||
|
||||
`SETGUI → protocan/uksvep.py (ctypes) → uksvep_can.c → общий payload balsam_can.c`.
|
||||
Ни GPIO, ни Qt, ни часы ОС общему C-профилю не нужны; динамическая память
|
||||
не используется. Вычисления и wire не дублируются в Python.
|
||||
|
||||
Все выходные буферы принадлежат caller и имеют явную capacity:
|
||||
|
||||
- `uksvep_can_encode`: минимум8 байтов, возвращает8 либо0;
|
||||
- `uksvep_can_decode`: минимум8 слов uint16 — направление, количество,
|
||||
затем максимум3 пары адрес/значение; возвращает число пар,0 для чужого ID,
|
||||
−1 для неверных аргументов/своего повреждённого кадра;
|
||||
- `uksvep_field_info`: минимум4 слова uint16 — адрес, максимум, default, flags;
|
||||
- `uksvep_evaluate_inputs`: минимум2 слова uint16 — неисправности и аварии.
|
||||
|
||||
Флаги полей: PARAM=1, COMMAND=2, STATE=4. GUI использует их для выбора
|
||||
допустимых редакторов роли. Имена возвращаются постоянными строками UTF-8;
|
||||
владение памятью клиенту не передаётся. Для применения RX клиент дополнительно
|
||||
отбрасывает STD/RTR и собственное направление/эхо. Физические уровни
|
||||
LED0…LED3 и PVT по текущему регистровому CAN не передаются; интерфейс не
|
||||
должен представлять расчётные лампы как измерение этих GPIO.
|
||||
|
||||
## Проверки
|
||||
|
||||
`test_uksvep_can` фиксирует независимые wire-эталоны, все16 адресов,
|
||||
малый выходной буфер, границы R127, sparse101, неверный DLC, чужие профили,
|
||||
особые входы и регрессию прежнего decoder Бальзам. Python-тесты используют
|
||||
ту же DLL, GUI-тесты подменяют только аппаратный драйвер.
|
||||
@@ -43,6 +43,9 @@ typedef struct {
|
||||
uint16_t values[BALSAM_CAN_REGISTER_COUNT];
|
||||
} balsam_can_frame_t;
|
||||
|
||||
size_t balsam_can_encode_payload(uint16_t address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity); /* Общий payload word2/word3/адрес/word1 для исторических регистровых CAN-профилей. */
|
||||
int balsam_can_decode_payload(const uint8_t *data, size_t size, balsam_can_frame_t *output); /* Разбирает только три слова и маску, оставляя маршрутизацию конкретному профилю. */
|
||||
|
||||
/** Return non-zero for the command/data/terminal IDs used by CAN_Bal_2812. */
|
||||
int balsam_can_is_id(uint32_t can_id);
|
||||
|
||||
|
||||
@@ -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. */
|
||||
@@ -0,0 +1,31 @@
|
||||
#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 /* Завершает публичный контракт шлюза. */
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef UKSVEP_CAN_H /* Защищает общий профиль ВЭП от повторного объявления. */
|
||||
#define UKSVEP_CAN_H /* Карта соответствует STM32-проекту UKSVEP_ICE_22220.6, а не ВЭП Бальзам. */
|
||||
#include "pcan_abi.h" /* Даёт экспортируемый C ABI для Python и других клиентов templates. */
|
||||
#ifdef __cplusplus /* Позволяет использовать тот же профиль из C++ и JNI. */
|
||||
extern "C" { /* Сохраняет стабильные имена экспортов без C++ mangling. */
|
||||
#endif /* Завершает выбор языка связывания. */
|
||||
#define UKSVEP_REGISTER_COUNT 128U /* message.h выделяет Modbus_LEN=0x80 слов uint16. */
|
||||
#define UKSVEP_FIELD_PARAM 1U /* Поле допускает запись параметра в реальный ВЭП. */
|
||||
#define UKSVEP_FIELD_COMMAND 2U /* Поле R127 содержит битовые команды приложения. */
|
||||
#define UKSVEP_FIELD_STATE 4U /* Поле состояния доступно для редактирования только в роли эмулируемого ВЭП. */
|
||||
PCAN_ABI_API uint32_t uksvep_can_id(unsigned device, unsigned from_device); /* Адрес 0…15: данные 0x1CE000+ID, команды 0x1CE020+ID; UINT32_MAX означает отказ. */
|
||||
PCAN_ABI_API size_t uksvep_can_encode(unsigned device, unsigned from_device, unsigned address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity); /* Строит DLC=8 для одного–трёх слов; capacity задаётся в байтах, требуется минимум 8. */
|
||||
PCAN_ABI_API int uksvep_can_decode(unsigned device, uint32_t id, const uint8_t *data, size_t size, uint16_t *output, size_t capacity); /* output: направление, число, три пары адрес/значение; capacity в словах uint16, минимум 8; 0=чужой ID, -1=ошибка. */
|
||||
PCAN_ABI_API unsigned uksvep_field_count(void); /* Число подтверждённых package.h полей, без выдуманных регистров. */
|
||||
PCAN_ABI_API int uksvep_field_info(unsigned index, unsigned device, uint16_t *output, size_t capacity); /* output[4]: адрес, максимум, начальное значение, флаги роли; capacity задаётся в словах uint16. */
|
||||
PCAN_ABI_API const char *uksvep_field_name(unsigned index); /* Возвращает постоянное имя UTF-8 без передачи владения памятью. */
|
||||
PCAN_ABI_API const char *uksvep_input_name(unsigned bit); /* Возвращает назначение и физический GPIO каждого из четырнадцати входов. */
|
||||
PCAN_ABI_API const char *uksvep_command_name(unsigned bit); /* Описывает только команды package.h; пустая строка означает резерв. */
|
||||
PCAN_ABI_API void uksvep_evaluate_inputs(uint16_t inputs, uint16_t alarm_mask, uint16_t error_mask, uint16_t *output, size_t capacity); /* Вычисляет R1/R2 по ReadEnteres; capacity задаётся в словах uint16, требуется минимум 2. */
|
||||
#ifdef __cplusplus /* Закрывает внешнее C-связывание для C++. */
|
||||
} /* Все клиенты вызывают одну реализацию протокола. */
|
||||
#endif /* Завершает совместимость с C++. */
|
||||
#endif /* Завершает публичный профиль UKSVEP. */
|
||||
@@ -48,11 +48,44 @@ int balsam_can_is_id(uint32_t can_id)
|
||||
&& relative < BALSAM_CAN_DATA_OFFSET + BALSAM_CAN_NODE_COUNT);
|
||||
}
|
||||
|
||||
size_t balsam_can_encode_payload(uint16_t address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity) /* Общий регистровый payload не зависит от выбранной платы. */
|
||||
{ /* Формат совпадает у Бальзам и UKSVEP, но адреса CAN определяют разные профили. */
|
||||
size_t index; /* Перебирает только присутствующие полезные слова. */
|
||||
uint16_t header; /* Хранит трёхбитную маску и 13-битный адрес регистра. */
|
||||
if (capacity < 8U || address > 0x1FFFU || values == NULL || output == NULL || count == 0U || count > 3U) { /* Проверяет геометрию payload до изменения выходных байтов. */
|
||||
return 0U; /* Отказ не создаёт частично готового кадра. */
|
||||
} /* Конкретный профиль дополнительно ограничивает размер своего банка. */
|
||||
memset(output, 0, 8U); /* Отсутствующие слова получают нулевое заполнение без бита присутствия. */
|
||||
header = (uint16_t)(address | (((7U << (3U - count)) & 7U) << 13U)); /* Одно/два/три слова дают маски 100/110/111 в битах 15…13. */
|
||||
output[4] = (uint8_t)(header >> 8U); /* Старший байт адреса/маски передаётся первым. */
|
||||
output[5] = (uint8_t)header; /* Младший байт завершает big-endian заголовок. */
|
||||
for (index = 0U; index < count; ++index) { /* eCAN передаёт word2 и word3 перед word1. */
|
||||
size_t offset = index == 0U ? 6U : (index - 1U) * 2U; /* Первое слово находится в байтах 6/7, следующие — в 0/1 и 2/3. */
|
||||
output[offset] = (uint8_t)(values[index] >> 8U); /* Записывает старшие восемь бит без зависимости от endian хоста. */
|
||||
output[offset + 1U] = (uint8_t)values[index]; /* Записывает младшие восемь бит того же слова. */
|
||||
} /* Порядок слов реализован в одном C-модуле для всех потребителей. */
|
||||
return 8U; /* Исторический регистровый кадр всегда имеет DLC=8. */
|
||||
} /* Завершает общий кодировщик трёх регистров. */
|
||||
|
||||
int balsam_can_decode_payload(const uint8_t *data, size_t size, balsam_can_frame_t *output) /* Отделяет общий payload от профиля CAN ID. */
|
||||
{ /* Маршрут и номер устройства заполняет вызывающий профиль. */
|
||||
uint16_t header; /* Получает маску присутствия и начальный адрес слова. */
|
||||
if (data == NULL || output == NULL || size != BALSAM_CAN_DLC) { /* Не читает неполные либо отсутствующие восемь байтов. */
|
||||
return 0; /* Отказ не изменяет ранее принятые значения клиента. */
|
||||
} /* Сохраняет исходный порядок декодирования Balsam без второго codec. */
|
||||
header = get_be16(&data[4]); /* Адрес находится в третьем 16-битном слове на проводе. */
|
||||
output->present_mask = (uint8_t)((header >> 13U) & 0x07U); /* Три старших бита разрешают отдельные регистры. */
|
||||
output->start_address = (uint16_t)(header & 0x1FFFU); /* Остальные 13 бит задают начальный адрес. */
|
||||
output->values[0] = get_be16(&data[6]); /* Первое логическое слово передаётся последним. */
|
||||
output->values[1] = get_be16(&data[0]); /* Второе логическое слово передаётся первым. */
|
||||
output->values[2] = get_be16(&data[2]); /* Третье логическое слово передаётся вторым. */
|
||||
return 1; /* Все три значения разобраны; применять можно только присутствующие. */
|
||||
} /* Завершает общий разбор payload без назначения платы. */
|
||||
|
||||
int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||
balsam_can_frame_t *output)
|
||||
{
|
||||
uint32_t relative;
|
||||
uint16_t header;
|
||||
if ((data == NULL) || (output == NULL)) return -1;
|
||||
can_id &= 0x1FFFFFFFUL;
|
||||
if (!balsam_can_is_id(can_id)) return 0;
|
||||
@@ -67,14 +100,7 @@ int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
|
||||
output->direction = can_id == 0x00745019UL
|
||||
? BALSAM_CAN_FROM_NODE : BALSAM_CAN_TO_NODE;
|
||||
}
|
||||
/* eCAN sends MDL before MDH: value2, value3, address/mask, value1. */
|
||||
header = get_be16(&data[4]);
|
||||
output->present_mask = (uint8_t)((header >> 13) & 0x07U);
|
||||
output->start_address = (uint16_t)(header & 0x1FFFU);
|
||||
output->values[0] = get_be16(&data[6]);
|
||||
output->values[1] = get_be16(&data[0]);
|
||||
output->values[2] = get_be16(&data[2]);
|
||||
return 1;
|
||||
return balsam_can_decode_payload(data, size, output); /* Переиспользует тот же payload decoder, сохраняя прежние проверки и маршрутизацию Бальзам. */
|
||||
}
|
||||
|
||||
const char *balsam_can_device_name(uint8_t device)
|
||||
|
||||
@@ -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 в первичный загрузчик. */
|
||||
@@ -0,0 +1,88 @@
|
||||
#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 и произвольных путей ОС. */
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "uksvep_can.h" /* Объявляет общий формат, метаданные и ABI профиля UKSVEP. */
|
||||
#include "balsam_can.h" /* Повторно использует исторический порядок трёх слов без второго кодека. */
|
||||
#include <string.h> /* Обнуляет кадр до заполнения присутствующих слов. */
|
||||
|
||||
typedef struct { /* Хранит подтверждённые параметры регистра из package.h/message.c. */
|
||||
uint16_t address; /* Индекс слова в modbus[128], без смещения Modbus 40001. */
|
||||
uint16_t maximum; /* Верхняя граница ввода в исходных единицах прошивки. */
|
||||
uint16_t initial; /* Значение для предпросмотра, не считается принятым от платы. */
|
||||
uint16_t flags; /* Отделяет параметры, команды и эмулируемое состояние. */
|
||||
const char *name; /* Постоянная подпись UTF-8 для всех потребителей профиля. */
|
||||
} field_t; /* Структура остаётся внутренней: через ABI передаются скаляры. */
|
||||
|
||||
static const field_t fields[] = { /* Источник адресов — Core/Inc/package.h текущего STM32-проекта. */
|
||||
{0U, 65535U, 0x247FU, UKSVEP_FIELD_STATE, "Дискретные входы"}, /* R0: нейтраль включает исправные питания, 380 В, ЛСУ и СКК; аварийные биты 8/9 выключены. */
|
||||
{1U, 65535U, 0U, UKSVEP_FIELD_STATE, "Неисправности"}, /* R1 вычисляется из входов и Alarm_mask, а не является прямым аппаратным входом. */
|
||||
{2U, 65535U, 0U, UKSVEP_FIELD_STATE, "Аварии"}, /* R2 вычисляется из входов и Error_mask. */
|
||||
{8U, 65535U, 0x277FU, UKSVEP_FIELD_PARAM, "Маска неисправностей"}, /* Default_params: 0x3FFF без битов 7, 11 и 12. */
|
||||
{9U, 65535U, 0x0200U, UKSVEP_FIELD_PARAM, "Маска аварий"}, /* По умолчанию выбран бит 9, авария сети 24 В. */
|
||||
{23U, 65535U, 0U, UKSVEP_FIELD_STATE, "Перемычки J0…J3 / TEST"}, /* Jumpers=R0x17, младший байт задаёт физические перемычки. */
|
||||
{48U, 65535U, 60U, UKSVEP_FIELD_PARAM, "Время до отключения, с"}, /* Millisecond умножает Sleep_time на 1000 мс. */
|
||||
{100U, 10U, 9U, UKSVEP_FIELD_PARAM, "Яркость лампы, 0…10"}, /* ШИМ использует десять отсчётов, штатное значение Brightness=9. */
|
||||
{101U, 65535U, 20U, UKSVEP_FIELD_PARAM, "Быстрый CAN, ×10 мс"}, /* Cancount[0]=20 соответствует 200 мс между пакетами. */
|
||||
{102U, 65535U, 200U, UKSVEP_FIELD_PARAM, "Медленный CAN, ×10 мс"}, /* Cancount[1]=200 соответствует 2000 мс между пакетами. */
|
||||
{125U, 65535U, 1U, UKSVEP_FIELD_STATE, "Версия прикладного протокола"}, /* PROTOKOL=1 относится к регистровому приложению, не к ProtoCAN Boot. */
|
||||
{126U, 65535U, 1U, UKSVEP_FIELD_STATE, "Mode = DeviceID + 1"}, /* При запросе метаданных значение уточняется по выбранному адресу. */
|
||||
{127U, 65535U, 0U, UKSVEP_FIELD_COMMAND, "Команды"} /* cTestLamp/cDefParam/cSaveParam/cLoadParam/cExtLamp/cExtLite/cReset. */
|
||||
}; /* Все адреса и значения по умолчанию определены в одном общем модуле. */
|
||||
|
||||
static const char *const inputs[] = { /* Порядок битов повторяет ReadEnteres в Core/Src/lampa.c. */
|
||||
"ИП1 · контроллер · PC13 · активный 0", /* Бит 0 читает инверсию IN_06. */
|
||||
"ИП2 · периферия · PC15 · активный 0", /* Бит 1 читает инверсию IN_05. */
|
||||
"ИП3 · замки / освещение · PA0 · активный 0", /* Бит 2 читает инверсию IN_04. */
|
||||
"ИП4 · датчики + · PA1 · активный 0", /* Бит 3 читает инверсию IN_03. */
|
||||
"ИП5 · датчики − · PA2 · активный 0", /* Бит 4 читает инверсию IN_02. */
|
||||
"ИП6 · драйверы · PA3 · активный 0", /* Бит 5 читает инверсию IN_01. */
|
||||
"Три фазы 380 В · PB15 · активный 0", /* Бит 6 читает инверсию IN_07. */
|
||||
"Заряд накопителя · PB14 · активный 0", /* Бит 7 не становится неисправностью либо аварией. */
|
||||
"Разряд накопителя · PB13 · активный 0", /* Бит 8 всегда становится неисправностью при единице. */
|
||||
"Авария 24 В · PB12 · активный 1", /* Бит 9 единственный читается без инверсии IN_10. */
|
||||
"Питание ЛСУ · PB1 · активный 0", /* Бит 10 читает инверсию IN_11. */
|
||||
"Резерв 11 · PB0 · активный 0", /* Бит 11 читает инверсию IN_12. */
|
||||
"Резерв 12 · PA5 · активный 0", /* Бит 12 читает инверсию IN_13. */
|
||||
"Питание СКК · PA4 · активный 0" /* Бит 13 читает инверсию IN_14. */
|
||||
}; /* Логическая единица здесь означает установленный бит R0, а не высокий GPIO. */
|
||||
|
||||
static const char *const commands[16] = { /* Имена битов берутся из package.h, без команд чужого задатчика. */
|
||||
"Тест ламп", "Параметры по умолчанию", "Сохранить параметры", "Загрузить параметры", /* Биты 0…3 действительно обслуживаются main. */
|
||||
"", "", "ExtLamp · флаг", "ExtLite · флаг", /* Биты 6/7 объявлены, но текущая логика лампы не использует их как отдельные команды. */
|
||||
"", "", "", "", "", "", "", "Перезагрузка" /* Бит 15 запрещает продление IWDG и вызывает аппаратный reset. */
|
||||
}; /* Пустые имена не создают ложных кнопок для резервных битов. */
|
||||
|
||||
uint32_t uksvep_can_id(unsigned device, unsigned from_device) /* Повторяет Setup_CAN_addr без связи с адресом boot Type=5. */
|
||||
{ /* Весь DeviceID ограничен четырьмя перемычками платы. */
|
||||
if (device > 15U || from_device > 1U) { /* Не обрезает ошибочный номер до адреса другого устройства. */
|
||||
return UINT32_MAX; /* Явный недопустимый ID исключает скрытую передачу. */
|
||||
} /* Направление 1 означает данные от эмулируемого или реального ВЭП. */
|
||||
return 0x001CE000UL | device | (from_device ? 0U : 0x20U); /* Командное смещение 0x20 отличается от 0x10 в Бальзам. */
|
||||
} /* Завершает единственное вычисление маршрутов UKSVEP. */
|
||||
|
||||
size_t uksvep_can_encode(unsigned device, unsigned from_device, unsigned address, const uint16_t *values, size_t count, uint8_t *output, size_t capacity) /* Проверяет геометрию до общего кодека. */
|
||||
{ /* Один кадр вмещает до трёх последовательных слов uint16. */
|
||||
if (capacity < 8U || uksvep_can_id(device, from_device) == UINT32_MAX || address >= UKSVEP_REGISTER_COUNT || count == 0U || count > 3U || count > UKSVEP_REGISTER_COUNT - address) { /* Исключает переполнение банка R0…R127. */
|
||||
return 0U; /* Не выдаёт частично собранный пакет ошибочного диапазона. */
|
||||
} /* Параметры относятся к существующей карте платы. */
|
||||
return balsam_can_encode_payload((uint16_t)address, values, count, output, capacity); /* Общий кодек задаёт порядок word2, word3, адрес/маска, word1. */
|
||||
} /* Не содержит второго endian/масочного алгоритма. */
|
||||
|
||||
int uksvep_can_decode(unsigned device, uint32_t id, const uint8_t *data, size_t size, uint16_t *output, size_t capacity) /* Разбирает только выбранное устройство и обе его роли. */
|
||||
{ /* ABI возвращает плоские скаляры без зависимости от выравнивания C-структур. */
|
||||
balsam_can_frame_t frame = {0}; /* Общий payload decoder заполняет маску, адрес и три слова. */
|
||||
unsigned index; /* Перебирает только реально присутствующие биты маски. */
|
||||
unsigned count = 0U; /* Считает адресные пары, которые разрешено применить к RX-снимку. */
|
||||
if (capacity < 8U || device > 15U || output == NULL || id > 0x1FFFFFFFUL) { /* Отвергает неверные аргументы до сравнения маршрутов. */
|
||||
return -1; /* Ошибка не должна частично изменить снимок клиента. */
|
||||
} /* Проверка чужого ID выполняется до DLC и указателя данных. */
|
||||
if (id != uksvep_can_id(device, 0U) && id != uksvep_can_id(device, 1U)) { /* Broadcast и чужой ВЭП не выдаются за адресный ответ. */
|
||||
return 0; /* Позволяет общему CAN-монитору передать кадр другим потребителям. */
|
||||
} /* Дальше кадр гарантированно адресует выбранный профиль. */
|
||||
if (data == NULL || size != 8U || !balsam_can_decode_payload(data, size, &frame) || frame.present_mask == 0U) { /* Требует полный classic CAN data payload с полезными словами. */
|
||||
return -1; /* Повреждённый собственный кадр учитывается как ошибка RX. */
|
||||
} /* Проверяет все адреса до публикации результата. */
|
||||
for (index = 0U; index < 3U; ++index) { /* В кадре максимум три последовательных адреса. */
|
||||
if ((frame.present_mask & (4U >> index)) != 0U && frame.start_address + index >= UKSVEP_REGISTER_COUNT) { /* Отсутствующее слово не создаёт ложной записи R128/R129. */
|
||||
return -1; /* При выходе присутствующего слова за карту отвергается весь пакет. */
|
||||
} /* Проверка проходит до заполнения плоского output. */
|
||||
} /* Геометрия всех значащих слов проверена. */
|
||||
output[0] = id == uksvep_can_id(device, 1U); /* Сохраняет реальное направление, чтобы собственный TX не стал ответом платы. */
|
||||
for (index = 0U; index < 3U; ++index) { /* Сохраняет только слова с установленными битами присутствия. */
|
||||
if ((frame.present_mask & (4U >> index)) != 0U) { /* Неприсутствующие нули не должны затирать предыдущие RX-значения. */
|
||||
output[2U + count * 2U] = (uint16_t)(frame.start_address + index); /* Возвращает точный индекс регистра. */
|
||||
output[3U + count * 2U] = frame.values[index]; /* Возвращает полезное uint16 без потери старшего бита. */
|
||||
++count; /* Продвигает плоскую таблицу только после полезной пары. */
|
||||
} /* Завершает применение одного бита маски. */
|
||||
} /* Все полезные слова готовы к атомарному обновлению модели. */
|
||||
output[1] = (uint16_t)count; /* Число пар делает ABI независимым от завершающих неиспользуемых элементов. */
|
||||
return (int)count; /* Положительный результат отличает собственный валидный кадр от чужого. */
|
||||
} /* Завершает общий декодер профиля без GPIO и Qt. */
|
||||
|
||||
unsigned uksvep_field_count(void) /* Предоставляет размер карты из единственного источника. */
|
||||
{ /* Клиент не хранит вторую таблицу адресов. */
|
||||
return (unsigned)(sizeof(fields) / sizeof(fields[0])); /* Размер определяется фактическим C-массивом. */
|
||||
} /* Завершает запрос количества полей. */
|
||||
|
||||
int uksvep_field_info(unsigned index, unsigned device, uint16_t *output, size_t capacity) /* Возвращает метаданные в явном массиве четырёх uint16. */
|
||||
{ /* Неверный адрес устройства не должен давать правдоподобные параметры другой платы. */
|
||||
if (capacity < 4U || index >= uksvep_field_count() || device > 15U || output == NULL) { /* Проверяет границы всех входных аргументов. */
|
||||
return 0; /* При отказе выходной буфер не изменяется. */
|
||||
} /* Индекс описывает существующее именованное поле. */
|
||||
output[0] = fields[index].address; /* Индекс слова modbus без дополнительного смещения. */
|
||||
output[1] = fields[index].maximum; /* Ограничение ввода остаётся общим для GUI и будущих портов. */
|
||||
output[2] = fields[index].address == 126U ? (uint16_t)(device + 1U) : fields[index].initial; /* Mode зависит от четырёх перемычек выбранного адреса. */
|
||||
if (fields[index].address == 23U) { /* R23 содержит активные логические перемычки J0…J3 в младшем полубайте. */
|
||||
output[2] = (uint16_t)device; /* Начальный эмулируемый адрес соответствует выбранному DeviceID без TEST. */
|
||||
} /* Профиль не создаёт противоречия между исходящим ID и состоянием перемычек. */
|
||||
output[3] = fields[index].flags; /* Роль определяет доступность редактирования реальной платы. */
|
||||
return 1; /* Все четыре скаляра успешно заполнены. */
|
||||
} /* Завершает запрос метаданных без передачи C padding через ABI. */
|
||||
|
||||
const char *uksvep_field_name(unsigned index) /* Возвращает UTF-8 имя именованного регистра. */
|
||||
{ /* Неверный индекс не должен приводить к чтению произвольного указателя. */
|
||||
return index < uksvep_field_count() ? fields[index].name : ""; /* Пустая строка явно означает отсутствие поля. */
|
||||
} /* Память строки остаётся во владении библиотеки. */
|
||||
|
||||
const char *uksvep_input_name(unsigned bit) /* Возвращает назначение физического дискретного входа. */
|
||||
{ /* В прошивке используются ровно четырнадцать разрядов R0. */
|
||||
return bit < 14U ? inputs[bit] : ""; /* Резервные 14/15 не получают выдуманных GPIO. */
|
||||
} /* Завершает запрос общей карты входов. */
|
||||
|
||||
const char *uksvep_command_name(unsigned bit) /* Возвращает назначение управляющего бита R127. */
|
||||
{ /* Сохраняет пустые позиции резервных команд package.h. */
|
||||
return bit < 16U ? commands[bit] : ""; /* Проверяет индекс до доступа к таблице. */
|
||||
} /* Завершает запрос именованных команд. */
|
||||
|
||||
void uksvep_evaluate_inputs(uint16_t value, uint16_t alarm_mask, uint16_t error_mask, uint16_t *output, size_t capacity) /* Модель ReadEnteres для исходящих данных эмулируемого ВЭП. */
|
||||
{ /* Расчёт используется только по явной кнопке и не считается ответом реальной платы. */
|
||||
if (output == NULL || capacity < 2U) { /* Позволяет безопасно отвергнуть неверный буфер клиента. */
|
||||
return; /* Не обращается к отсутствующей памяти. */
|
||||
} /* Все действия ниже повторяют особые случаи битов 7/8/9 из lampa.c. */
|
||||
output[0] = (uint16_t)((uint16_t)~value & alarm_mask & (uint16_t)~0x0380U); /* Обычные неисправности — отсутствие включённых в маску входов; 7/8/9 обрабатываются отдельно. */
|
||||
output[0] |= (uint16_t)(value & 0x0300U); /* Разряд и авария 24 В всегда являются неисправностями. */
|
||||
output[1] = (uint16_t)((uint16_t)~value & error_mask & (uint16_t)~0x0380U); /* Обычные аварии также определяются инверсией входов и маской. */
|
||||
output[1] |= (uint16_t)((value & error_mask & 0x0100U) | (value & 0x0200U)); /* Разряд зависит от Error_mask.8, авария 24 В всегда активна при R0.9=1. */
|
||||
} /* Заряд накопителя R0.7 никогда не создаёт неисправность либо аварию. */
|
||||
@@ -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, §ors) == 0 && entry == 0x9000 && sectors == 0); /* RAM-ядро должно иметь реально загруженный entry и не включать Flash-секторы. */
|
||||
CHECK(c2000_sci_validate(flash, sizeof(flash), 1, &entry, §ors) == 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. */
|
||||
@@ -0,0 +1,22 @@
|
||||
#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 проверяются отдельными сценариями. */
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "uksvep_can.h" /* Проверяет C99-профиль, который вызывают все GUI через ABI. */
|
||||
#include "balsam_can.h" /* Сохраняет регрессию существующей маршрутизации Бальзам после выделения общего payload. */
|
||||
#include <assert.h> /* Неверный проводной вектор должен завершать тест с отказом. */
|
||||
#include <string.h> /* Сравнивает байты с независимым эталоном, а не только с обратным decoder. */
|
||||
|
||||
int main(void) /* Фиксирует реальную карту UKSVEP из can.c/lampa.c/package.h. */
|
||||
{ /* Проверки не требуют Qt, аппаратного CAN или микроконтроллера. */
|
||||
const uint16_t words[] = {0x1234U, 0xABCDU, 0x8001U}; /* Различные слова выявляют перестановку и потерю старшего бита. */
|
||||
const uint8_t golden[] = {0xABU, 0xCDU, 0x80U, 0x01U, 0xE0U, 0x08U, 0x12U, 0x34U}; /* R8…R10: word2, word3, маска111/адрес8, word1. */
|
||||
const uint8_t sparse[] = {0xAAU, 0x55U, 0x33U, 0x44U, 0xA0U, 0x7DU, 0x11U, 0x22U}; /* Маска101 разрешает R125 и R127, оставляя R126 неизменным. */
|
||||
uint8_t payload[8]; /* Выходной буфер реального кодировщика C. */
|
||||
uint16_t decoded[8]; /* Плоский ABI буфер направления и пар регистров. */
|
||||
uint16_t info[4]; /* Адрес, максимум, начальное значение и роль регистра. */
|
||||
uint16_t state[2]; /* Расчётные неисправности и аварии эмулируемого ВЭП. */
|
||||
balsam_can_frame_t previous = {0}; /* Независимо проверяет существующий публичный decoder Бальзам. */
|
||||
unsigned device; /* Перебирает все положения четырёх перемычек. */
|
||||
unsigned index; /* Перебирает общую карту подтверждённых полей. */
|
||||
|
||||
for (device = 0U; device < 16U; ++device) { /* Адреса не должны наследовать Mode=9 и базу BA0000 другого ВЭП. */
|
||||
assert(uksvep_can_id(device, 0U) == 0x001CE020U + device); /* Командный ID берётся из Setup_CAN_addr текущего STM32-проекта. */
|
||||
assert(uksvep_can_id(device, 1U) == 0x001CE000U + device); /* Исходящий ID ВЭП не содержит командного смещения 0x20. */
|
||||
} /* Проверяет обе роли каждого физического адреса. */
|
||||
assert(uksvep_can_id(16U, 0U) == UINT32_MAX && uksvep_can_id(0U, 2U) == UINT32_MAX); /* Ошибочная цель не обрезается до соседнего устройства. */
|
||||
assert(uksvep_can_encode(5U, 0U, 8U, words, 3U, payload, 7U) == 0U); /* Малый выходной буфер не допускает записи восьми байтов за его границы. */
|
||||
assert(uksvep_can_encode(5U, 0U, 8U, words, 3U, payload, sizeof(payload)) == 8U); /* Строит стандартный трёхсловный командный кадр. */
|
||||
assert(memcmp(payload, golden, sizeof(golden)) == 0); /* Эталон проверяет endian, адрес и маску независимо от decoder. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE025U, golden, 8U, decoded, 8U) == 3); /* Распознаёт собственное командное направление. */
|
||||
assert(decoded[0] == 0U && decoded[2] == 8U && decoded[3] == 0x1234U && decoded[6] == 10U && decoded[7] == 0x8001U); /* Возвращает исходные пары без потери разрядов. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE005U, sparse, 8U, decoded, 8U) == 2); /* Частичный RX не требует всех трёх последовательных регистров. */
|
||||
assert(decoded[0] == 1U && decoded[2] == 125U && decoded[3] == 0x1122U && decoded[4] == 127U && decoded[5] == 0x3344U); /* Отсутствующий R126 не появляется среди обновлений. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE004U, golden, 8U, decoded, 8U) == 0); /* Данные другого адреса игнорируются. */
|
||||
assert(uksvep_can_decode(5U, 0x00BA0018U, golden, 8U, decoded, 8U) == 0); /* Старый ВЭП проекта Бальзам не подменяет STM32 УКСВЭП. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE005U, golden, 7U, decoded, 8U) == -1); /* Повреждённый DLC собственного узла отвергается. */
|
||||
assert(uksvep_can_encode(5U, 0U, 127U, words, 2U, payload, sizeof(payload)) == 0U); /* Запись второго слова не может пересечь R127. */
|
||||
assert(uksvep_can_encode(5U, 0U, 127U, words, 1U, payload, sizeof(payload)) == 8U && payload[4] == 0x80U && payload[5] == 0x7FU); /* Одно последнее слово использует маску100 и нулевые соседи. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE005U, payload, 8U, decoded, 8U) == 1); /* Последний валидный регистр можно принять отдельно. */
|
||||
payload[4] = 0xE0U; /* Искусственно делает присутствующими запрещённые R128/R129. */
|
||||
assert(uksvep_can_decode(5U, 0x001CE005U, payload, 8U, decoded, 8U) == -1); /* Такой пакет отвергается целиком, без частичного применения R127. */
|
||||
assert(balsam_can_decode(0x00BA0017U, golden, 8U, &previous) == 1 && previous.device == 8U && previous.values[1] == 0xABCDU); /* Общий payload сохраняет прежний маршрут и слова задатчика. */
|
||||
for (index = 0U; index < uksvep_field_count(); ++index) { /* Каждое именованное поле должно принадлежать реальной карте. */
|
||||
assert(uksvep_field_info(index, 7U, info, 4U) && info[0] < 128U && info[2] <= info[1]); /* Начальное значение не превышает UI-границу поля. */
|
||||
assert(strlen(uksvep_field_name(index)) != 0U); /* Нет безымянных полей, которые выглядели бы пропущенной картой. */
|
||||
if (info[0] == 126U) { /* Физический адрес отличается от Mode на единицу. */
|
||||
assert(info[2] == 8U); /* DeviceID7 соответствует Mode8 независимо от названия устройства. */
|
||||
} /* Остальные defaults одинаковы для всех адресов. */
|
||||
} /* Размер карты задаёт C, а не отдельный список Python. */
|
||||
assert(!uksvep_field_info(999U, 0U, info, 4U) && strlen(uksvep_input_name(14U)) == 0U); /* За границами нет фиктивных регистров и GPIO. */
|
||||
assert(strstr(uksvep_input_name(9U), "PB12") != NULL && strlen(uksvep_command_name(4U)) == 0U); /* Особый прямой вход и резервная команда сохраняют реальное назначение. */
|
||||
uksvep_evaluate_inputs(0x247FU, 0x277FU, 0x0200U, state, 2U); /* Моделирует исправные штатные входы и маски. */
|
||||
assert(state[0] == 0U && state[1] == 0U); /* Исправное состояние не должно вызывать ложную аварию. */
|
||||
uksvep_evaluate_inputs(0x247EU, 0x277FU, 0x0200U, state, 2U); /* Убирает только исправность ИП1. */
|
||||
assert(state[0] == 1U && state[1] == 0U); /* Маска по умолчанию делает потерю ИП1 неисправностью, а не аварией. */
|
||||
uksvep_evaluate_inputs(0x27FFU, 0U, 0U, state, 2U); /* Включает заряд, разряд и аварию 24 В при нулевых масках. */
|
||||
assert(state[0] == 0x0300U && state[1] == 0x0200U); /* Заряд не влияет, разряд всегда неисправность, 24 В всегда авария. */
|
||||
uksvep_evaluate_inputs(0x25FFU, 0U, 0x0100U, state, 2U); /* Разрешает аварийный разряд отдельным битом маски. */
|
||||
assert(state[1] == 0x0100U); /* Особый бит 8 проверяется по своей полярности без инверсии. */
|
||||
return 0; /* Все эталоны, границы и регрессия старого профиля выполнены. */
|
||||
} /* Завершает независимую host-проверку C-профиля. */
|
||||
@@ -24,6 +24,8 @@ INCLUDE = ROOT / "include"
|
||||
JNI_INCLUDES: list[Path] = []
|
||||
SOURCES = [
|
||||
ROOT / "src" / name for name in (
|
||||
"c2000_sci.c", # Прямая MSVC/CC-сборка включает общий автомат SCI F28335.
|
||||
"set_modbus_gateway.c", # Экспортирует общий USB RTU/TCP кодек также при прямой MSVC/CC сборке без CMake.
|
||||
"deif_xdi.c", # Общий DEIF входит в host-библиотеку.
|
||||
"set_invertor_gas.c", # Собирает общий адаптер двух GAS для STM Invertor при прямом запуске host-builder.
|
||||
"set_deif_gauge.c", "set_igbt_realtime.c",
|
||||
@@ -32,6 +34,7 @@ SOURCES = [
|
||||
"altera_stream.c",
|
||||
"set_gas_watch.c", # Добавляет общий GAS_WATCH2 в прямые MSVC/CC сборки, чтобы новые ctypes/JNI символы были доступны.
|
||||
"set_protocol.c", "set_can.c", "set_firmware.c", "set_telemetry.c", "set_plot.c", "set_trends.c", "set_spectrum.c", "set_signal.c", "set_wavegen.c",
|
||||
"uksvep_can.c", # Включает общий профиль ВЭП также в прямую MSVC/CC сборку DLL.
|
||||
"balsam_can.c", "set_crc.c", "periph28335.c", "tms2812.c", "gui_catalog.c", "gui_frame.c", "pcan_abi.c", "pcan_crc.c",
|
||||
"pcan_frame.c", "pcan_id.c", "pcan_link.c", "pcan_ring.c",
|
||||
"pcan_gas.c", "pcan_modbus_server.c",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# Прошивка STM32 через J-Link
|
||||
|
||||
В SETGUI откройте **Сервис → Прошивка** и выберите **STM32 / J-Link JTAG или SWD**.
|
||||
Установленный SEGGER J-Link Commander обнаруживается в Keil, каталоге SEGGER или PATH.
|
||||
При необходимости укажите `JLink.exe` кнопкой «Обзор». Одноимённый инструмент Java JDK программатором не является.
|
||||
|
||||
Выберите микроконтроллер из базы J-Link, JTAG либо SWD, частоту в кГц и фактический объём Flash в КиБ.
|
||||
Начальная частота — 1000 кГц. При нескольких подключённых J-Link укажите серийный номер нужного программатора.
|
||||
COM-порт и доступ к BOOT0 для этого режима не требуются.
|
||||
|
||||
Выберите HEX или BIN и нажмите «Начать прошивку». HEX сохраняет собственные абсолютные адреса и разрывы;
|
||||
для BIN укажите адрес в поле «Адрес приложения». При изменении файла после выбора требуется выбрать его заново.
|
||||
Если есть manifest выпуска, его target должен соответствовать выбранному MCU.
|
||||
|
||||
Каждая операция создаёт отдельный каталог в выбранной папке резервных копий:
|
||||
|
||||
- `before.bin` — вся Flash до записи;
|
||||
- `image.hex` — проверенный разреженный снимок записываемых данных;
|
||||
- `after.bin` — вся Flash после записи;
|
||||
- `program.jlink`, `program.log`, `program-dll.log` — команды и результаты программирования;
|
||||
- `run.jlink`, `run.log`, `run-dll.log` — отдельный этап сброса и запуска.
|
||||
|
||||
Программирование выполняется через `loadfile` без команды полного стирания.
|
||||
После встроенной проверки J-Link выполняется независимое побайтное сравнение readback с планом.
|
||||
Только после успешного сравнения отдельный процесс делает reset и `g`.
|
||||
Нулевой код выхода без полных снимков Flash не считается успешной записью.
|
||||
Отмена останавливает собственный процесс Commander; при прерванной записи нужна повторная прошивка.
|
||||
|
||||
## UKSVEP STM32F103RC
|
||||
|
||||
Для текущей платы оставьте включённым профиль UKSVEP, устройство `STM32F103RC`, объём 256 КиБ.
|
||||
Приложение начинается с `0x08008000`. Страница параметров `0x0801F800..0x0801FFFF` запрещена для записи,
|
||||
её сохранность проверяется сравнением `before.bin` и `after.bin`.
|
||||
|
||||
Для первой установки выберите разреженный `uksvep_install.hex`, содержащий boot и приложение.
|
||||
Для обновления через JTAG можно выбрать обычный HEX приложения или BIN с адресом `0x08008000`.
|
||||
Общий план добавит manifest CRC32 по адресу `0x0803F800`, который проверяет постоянный загрузчик.
|
||||
Готовый manifest установочного HEX сверяется с приложением. Перед запуском снимаются старые одноразовые
|
||||
маркеры boot в BKP DR9/DR10 без стирания резервного домена.
|
||||
|
||||
Для других плат снимите флажок UKSVEP и задайте их микроконтроллер и объём Flash.
|
||||
Защита страницы и формирование manifest UKSVEP относятся только к этому явно выбранному профилю.
|
||||
|
||||
## Расположение общей реализации
|
||||
|
||||
- `python/set_devices/stm32_jtag.py` — неизменяемые настройки, проверка образа, разреженный HEX, сценарии и readback;
|
||||
- `python/set_devices/qt_ports/stm32_jtag.py` — асинхронный порт QProcess, журналы, тайм-аут, отмена и reset/run;
|
||||
- `python/set_devices/qt_ports/stm32_jtag_panel.py` — встраиваемая форма Qt 5/6 с сохранением настроек;
|
||||
- `python/tests/test_stm32_jtag.py` — проверки плана, защиты параметров и ошибок без физического MCU.
|
||||
|
||||
Модули не импортируют `gui_desktop`. SETGUI подключает панель к существующим кнопкам, прогрессу и терминалу.
|
||||
Код загрузчика `stm32-jtag`, транспорт `stm32`, соответствует индексу 8 формы;
|
||||
профиль `flashing.profiles.stm32-jtag` может задавать `base_address`.
|
||||
Параметры конкретного J-Link сохраняются локально и не публикуются вместе с прошивкой.
|
||||
SEGGER Commander должен быть установлен отдельно; пакет SETGUI не скачивает и не включает сторонний executable.
|
||||
@@ -0,0 +1,146 @@
|
||||
"""Расчёт токов асинхронного двигателя по состояниям ключей инвертора.""" # Модель не зависит от GUI, sigrok и сторонних пакетов Python.
|
||||
import cmath # Комплексные числа представляют неподвижные электрические оси α и β.
|
||||
import math # Проверки параметров и преобразования СИ используют стандартную библиотеку.
|
||||
|
||||
|
||||
def finite(value, name, minimum=None, strict=False): # Единая проверка исключает NaN, бесконечности и нефизические параметры.
|
||||
number = float(value) # Параметры sigrok и непосредственных вызовов приводятся к одному числовому типу.
|
||||
if not math.isfinite(number) or minimum is not None and (number <= minimum if strict else number < minimum): # Строгая граница применяется к сопротивлениям и индуктивностям.
|
||||
raise ValueError('{} has an invalid value: {}'.format(name, value)) # Сообщение указывает конкретную настройку пользователя.
|
||||
return number # Возвращается только проверенное значение в единицах вызывающей стороны.
|
||||
|
||||
|
||||
def clarke(values): # Амплитудно-инвариантное преобразование сохраняет амплитуду симметричных фаз.
|
||||
return complex((2 * values[0] - values[1] - values[2]) / 3, (values[1] - values[2]) / math.sqrt(3)) # Оси αβ соответствуют порядку фаз A, B, C.
|
||||
|
||||
|
||||
def phases(vector): # Обратное преобразование восстанавливает три фазы трёхпроводной нагрузки.
|
||||
return (vector.real, -vector.real / 2 + math.sqrt(3) * vector.imag / 2, -vector.real / 2 - math.sqrt(3) * vector.imag / 2) # Сумма фаз равна нулю по первому закону Кирхгофа.
|
||||
|
||||
|
||||
class InductionMotor: # Линейная модель короткозамкнутого двигателя использует приведённые к статору параметры ротора.
|
||||
def __init__(self, rs=1.0, rr=0.8, lls_mh=5.0, llr_mh=5.0, lm_mh=150.0, pole_pairs=2, speed_rpm=1440.0, initial_flux=0.0, flux_angle=0.0, ia=0.0, ib=0.0): # Значения по умолчанию являются примером, а не паспортом конкретного двигателя.
|
||||
self.rs = finite(rs, 'Stator R (ohm)', 0, True) # Положительное сопротивление одной обмотки статора задаётся в омах.
|
||||
self.rr = finite(rr, 'Rotor R referred to stator (ohm)', 0, True) # Сопротивление ротора должно быть приведено к той же обмотке статора.
|
||||
lls = finite(lls_mh, 'Stator leakage L (mH)', 0, True) / 1000 # Миллигенри переводятся в генри перед вычислением уравнений.
|
||||
llr = finite(llr_mh, 'Rotor leakage L (mH)', 0, True) / 1000 # Параметр ротора также приводится к статору и переводится в генри.
|
||||
lm = finite(lm_mh, 'Magnetizing L (mH)', 0, True) / 1000 # Взаимная индуктивность задаёт магнитную связь без насыщения.
|
||||
pairs = finite(pole_pairs, 'Pole pairs', 1) # Число пар полюсов переводит механическую скорость в электрическую.
|
||||
if pairs != int(pairs): # Дробное число пар полюсов не описывает физический двигатель.
|
||||
raise ValueError('Pole pairs must be an integer.') # Ошибка выдаётся до расчёта токов.
|
||||
omega = finite(speed_rpm, 'Mechanical speed (rpm)') * pairs * math.tau / 60 # Один оборот равен 2π радиан, одна минута содержит 60 секунд.
|
||||
lr = llr + lm # Полная индуктивность ротора включает рассеяние и намагничивание.
|
||||
self.lsigma = lls + lm * llr / lr # Устойчивая форма Ls−Lm²/Lr не теряет точность при малом рассеянии.
|
||||
self.k = lm / lr # Коэффициент связи переводит потокосцепление ротора в уравнение статора.
|
||||
self.a = -(self.rs + self.rr * self.k * self.k) / self.lsigma # Первый диагональный коэффициент учитывает потери статора и приведённого ротора.
|
||||
self.b = self.k * complex(self.rr / lr, -omega) / self.lsigma # Влияние потока ротора включает его вращение относительно неподвижных осей.
|
||||
self.c = self.rr * self.k # Ток статора возбуждает поток ротора через взаимную индуктивность.
|
||||
self.d = complex(-self.rr / lr, omega) # Поток ротора затухает с постоянной Lr/Rr и вращается с электрической скоростью ротора.
|
||||
self.det = self.a * self.d - self.b * self.c # Определитель нужен для точного вынужденного решения при постоянном напряжении.
|
||||
self.mid = (self.a + self.d) / 2 # Половина следа матрицы задаёт среднее собственных значений.
|
||||
self.root = cmath.sqrt(((self.a - self.d) / 2) ** 2 + self.b * self.c) # Разность собственных значений позволяет вычислить матричную экспоненту без numpy.
|
||||
initial = (finite(ia, 'Initial Ia (A)'), finite(ib, 'Initial Ib (A)')) # Начальные линейные токи задаются в амперах.
|
||||
self.current = clarke((initial[0], initial[1], -sum(initial))) # Третья фаза определяется условием трёхпроводного подключения.
|
||||
self.flux = finite(initial_flux, 'Initial rotor flux linkage (Wb)', 0) * cmath.exp(1j * math.radians(finite(flux_angle, 'Initial rotor flux angle (deg)'))) # Модуль и угол задают неизвестное при начале записи начальное потокосцепление.
|
||||
self.cache = {} # Повторяющиеся интервалы переключений используют готовую матрицу перехода.
|
||||
|
||||
def advance(self, seconds, voltage): # Между фронтами ключей напряжение неизменно, поэтому используется точное линейное решение.
|
||||
dt = finite(seconds, 'Time interval (s)', 0) # Отрицательное время и неконечные интервалы запрещены.
|
||||
if not dt: # Повторные события одного отсчёта меняют ключи без изменения физического состояния.
|
||||
return phases(self.current) # Начальный ток сохраняется без искусственного шага интегрирования.
|
||||
matrix = self.cache.get(dt) # На равномерной сетке большинство длительностей повторяется.
|
||||
if matrix is None: # Новая длительность требует вычисления exp(A·dt) ровно один раз.
|
||||
if abs(self.root * dt) < 1e-8: # При близких собственных значениях деление на их разность теряет точность.
|
||||
factor = cmath.exp(self.mid * dt) # Общая экспонента остаётся ограниченной для пассивного двигателя.
|
||||
ratio = factor * dt * (1 + (self.root * dt) ** 2 / 6) # Разложение sinh(z)/z сохраняет точность около нуля.
|
||||
center = factor * (1 + (self.root * dt) ** 2 / 2) # Разложение cosh(z) согласовано с тем же пределом.
|
||||
else: # Раздельные экспоненты избегают переполнения cosh при сильно различающихся постоянных времени.
|
||||
first, second = cmath.exp((self.mid + self.root) * dt), cmath.exp((self.mid - self.root) * dt) # Обе моды учитывают затухание и вращение потока ротора.
|
||||
ratio, center = (first - second) / (2 * self.root), (first + second) / 2 # Эти коэффициенты задают экспоненту матрицы размерности два.
|
||||
matrix = (center + ratio * (self.a - self.mid), ratio * self.b, ratio * self.c, center + ratio * (self.d - self.mid)) # Порядок элементов соответствует строкам тока статора и потока ротора.
|
||||
if len(self.cache) >= 1024: # Ограничение в 1024 различных длительности исключает рост памяти на нерегулярном ШИМ.
|
||||
self.cache.clear() # Сброс кэша влияет только на производительность, а не на состояние двигателя.
|
||||
self.cache[dt] = matrix # Сохраняется только матрица перехода, независимая от текущего напряжения.
|
||||
drive = clarke(voltage) / self.lsigma # Фазные напряжения переводятся в неподвижный вектор скорости изменения тока.
|
||||
equilibrium_i, equilibrium_flux = -self.d * drive / self.det, self.c * drive / self.det # Частное решение −A⁻¹B·u учитывает постоянное напряжение на всём интервале.
|
||||
delta_i, delta_flux = self.current - equilibrium_i, self.flux - equilibrium_flux # Начальное состояние переносится относительно частного решения.
|
||||
self.current = equilibrium_i + matrix[0] * delta_i + matrix[1] * delta_flux # Ток обновляется без явного Эйлера и зависимости устойчивости от частоты отображения.
|
||||
self.flux = equilibrium_flux + matrix[2] * delta_i + matrix[3] * delta_flux # Поток использует прежний ток, сохранённый до обновления состояния.
|
||||
if not all(math.isfinite(value) for value in (self.current.real, self.current.imag, self.flux.real, self.flux.imag)): # Чрезмерные параметры не должны порождать молчаливые NaN в графиках.
|
||||
raise ValueError('Motor state overflow: check motor parameters and capture timing.') # Сообщение связывает ошибку с физическими параметрами и временем записи.
|
||||
return phases(self.current) # Возвращаются мгновенные токи трёх эквивалентных обмоток в амперах.
|
||||
|
||||
|
||||
class PwmMotor: # Связь логических входов, силовой схемы и общего двигателя не содержит зависимости от DSView.
|
||||
def __init__(self, samplerate, topology='2-level', dc_voltage=560.0, midpoint_percent=50.0, connection='star', active_high=True, **motor): # Полное напряжение звена задаётся между положительной и отрицательной шинами.
|
||||
self.rate = finite(samplerate, 'Samplerate (Hz)', 0, True) # Частота захвата переводит номера отсчётов в секунды.
|
||||
if topology not in ('2-level', '3-level P/N', '3-level NPC') or connection not in ('star', 'delta'): # Поддерживаются явные схемы без автоматического угадывания кодировки.
|
||||
raise ValueError('Unsupported inverter topology or winding connection.') # Неизвестная схема не подменяется другой электрической моделью.
|
||||
dc = finite(dc_voltage, 'Total DC link (V)', 0, True) # Значение в вольтах относится ко всему звену, а не к его половине.
|
||||
split = finite(midpoint_percent, 'Upper DC link (%)', 0, True) / 100 # Доля верхней половины позволяет моделировать дисбаланс конденсаторов.
|
||||
if split >= 1: # Обе половины звена должны иметь строго положительное напряжение.
|
||||
raise ValueError('Upper DC link fraction must be between 0 and 100 percent.') # Граничные значения не описывают трёхуровневое звено.
|
||||
self.upper, self.lower = dc * split, -dc * (1 - split) # Потенциалы полюсов отсчитываются от средней точки конденсаторов.
|
||||
self.topology, self.connection, self.active_high = topology, connection, bool(active_high) # Параметры сохраняются для интерпретации каждой новой комбинации ключей.
|
||||
ia, ib = motor.get('ia', 0.0), motor.get('ib', 0.0) # Начальные токи пользователь задаёт в линейных проводах независимо от схемы обмоток.
|
||||
if connection == 'delta': # В симметричном треугольнике линейный ток равен разности токов соседних обмоток.
|
||||
motor.update(ia=(2 * ia + ib) / 3, ib=(ib - ia) / 3) # Обратное преобразование предполагает отсутствие циркулирующего тока нулевой последовательности.
|
||||
self.motor = InductionMotor(**motor) # Параметры Rr и Lr всегда приведены к одной фактической обмотке статора.
|
||||
self.sample, self.pins, self.poles = None, None, (0.0, 0.0, 0.0) # До первого отсчёта не создаётся вымышленный участок записи.
|
||||
self.warnings = () # Диагностика сообщает о комбинациях, не задающих однозначное выходное напряжение.
|
||||
self.interval_warnings = () # Завершённый интервал сохраняет собственную диагностику до приёма следующего состояния затворов.
|
||||
|
||||
def currents(self): # Публичные результаты относятся к проводам A, B, C, подключённым к инвертору.
|
||||
a, b, c = phases(self.motor.current) # Эквивалентные токи обмоток сохраняют условие отсутствия нулевой последовательности.
|
||||
return (a - c, b - a, c - b) if self.connection == 'delta' else (a, b, c) # Для треугольника выводятся линейные токи, а не токи внутри обмоток.
|
||||
|
||||
def set_gates(self, pins): # Нумерация: Q1/P фаз A/B/C, Q4/N фаз A/B/C, затем Q2/Q3 каждой фазы.
|
||||
if len(pins) != 12 or any(value not in (0, 1, None) for value in pins): # Отсутствующий логический канал обозначается None до нормализации полярности.
|
||||
raise ValueError('Expected twelve gate inputs containing 0, 1 or None.') # Служебные значения sigrok не принимаются за реальный логический уровень.
|
||||
bits = tuple(None if value is None else bool(value) == self.active_high for value in pins) # Инверсия применяется только к реально назначенным входам.
|
||||
required = range(3) if self.topology == '2-level' else range(6) if self.topology == '3-level P/N' else range(12) # Три, шесть или двенадцать каналов нужны выбранной схеме.
|
||||
if any(bits[index] is None for index in required): # Неполная трёхфазная схема не позволяет определить линейные напряжения двигателя.
|
||||
raise ValueError('Assign all gate channels required by the selected topology.') # Ошибка возникает до продолжения расчёта по отсутствующим сигналам.
|
||||
if self.topology == '2-level' and any(value is not None for value in bits[3:6]) and any(value is None for value in bits[3:6]): # Нижние ключи двухуровневой схемы назначаются одновременно для всех фаз.
|
||||
raise ValueError('Assign either all three lower gates or none.') # Частично измеренные нижние ключи не смешиваются с идеальными дополнениями.
|
||||
poles, warnings = [], [] # Каждая фаза сначала получает собственный потенциал и список неоднозначностей.
|
||||
currents = self.currents() # Направление линейного тока задаёт приближённую проводимость обратных диодов во время dead time.
|
||||
for phase in range(3): # Все входы одного отсчёта обрабатываются одновременно, без ложных промежуточных состояний.
|
||||
high, low = bits[phase], bits[phase + 3] # В P/N это команды положительного и отрицательного уровней; в NPC это внешние Q1 и Q4.
|
||||
state = (high, low) # Стандартная таблица P/N использует только два внешних логических входа.
|
||||
table = {(True, False): self.upper, (False, True): self.lower, (False, False): 0.0} # Состояние 00 задаёт среднюю точку только в явной кодировке P/N.
|
||||
if self.topology == '2-level': # Один вход на фазу означает идеальную комплементарную пару без измеренного dead time.
|
||||
if low is None: # Нижняя команда восстанавливается только когда пользователь не назначил соответствующие входы.
|
||||
low = not high # Низкий уровень верхней команды соответствует отрицательной шине.
|
||||
state = (high, low) # Реальные нижние команды сохраняют интервалы одновременного закрытия ключей.
|
||||
table = {(True, False): self.upper, (False, True): self.lower} # Одновременное включение двух ключей не имеет допустимого фазного напряжения.
|
||||
if self.topology == '3-level NPC': # NPC обрабатывает четыре отдельных затворных сигнала каждого плеча.
|
||||
state = (high, bits[6 + 2 * phase], bits[7 + 2 * phase], low) # Физический порядок ключей идёт сверху вниз от положительной шины к отрицательной.
|
||||
table = {(True, True, False, False): self.upper, (False, True, True, False): 0.0, (False, False, True, True): self.lower} # Допустимые состояния Q1…Q4 равны 1100, 0110 и 0011.
|
||||
if state in table: # Однозначная команда непосредственно определяет полюсное напряжение.
|
||||
pole = table[state] # Токи интегрируются по реальному напряжению, а не по сглаженному заполнению ШИМ.
|
||||
elif not any(state): # Полностью закрытое плечо проводит через обратные диоды при ненулевом токе.
|
||||
pole = self.lower if currents[phase] > 1e-9 else self.upper if currents[phase] < -1e-9 else 0.0 # Порог 1 нА подавляет выбор шины по ошибке округления около нулевого тока.
|
||||
warnings.append('Phase {}: diode/dead-time approximation'.format('ABC'[phase])) # Неизвестные падения на диодах и смена проводимости явно отмечаются как приближение.
|
||||
else: # Недопустимые перекрытия и неполные состояния NPC не интерпретируются как достоверное напряжение.
|
||||
pole = self.poles[phase] # Последний определённый потенциал используется только как явно помеченное продолжение оценки.
|
||||
warnings.append('Phase {}: invalid gates {}; current estimate unreliable'.format('ABC'[phase], ''.join(str(int(value)) for value in state))) # Диагностика содержит фазу и фактическую комбинацию ключей.
|
||||
poles.append(pole) # Все три полюса сохраняются до вычисления напряжений обмоток.
|
||||
self.pins, self.poles, self.warnings = bits, tuple(poles), tuple(warnings) # Состояние относится к интервалу после текущего одновременного переключения.
|
||||
|
||||
def update(self, sample, pins=None): # Сначала завершается предыдущий электрический интервал и только затем меняются ключи.
|
||||
stamp = int(sample) # Номера отсчётов сохраняются целыми, чтобы исключить накопление ошибки временной сетки.
|
||||
if stamp != sample or stamp < 0 or self.sample is not None and stamp < self.sample: # Отсчёты должны поступать в хронологическом порядке.
|
||||
raise ValueError('Sample numbers must be nonnegative, integral and monotonic.') # Ошибка входной последовательности не маскируется нулевой длительностью.
|
||||
previous, start = self.currents(), self.sample # Левый конец сегмента берётся до интегрирования предыдущего напряжения.
|
||||
self.interval_warnings = self.warnings # Выходной пакет должен описывать ключи рассчитанного интервала, а не уже принятую новую команду.
|
||||
if start is not None and stamp > start: # Первый отсчёт определяет только начальное состояние записи.
|
||||
a, b, c = self.poles # Полюсные напряжения неизменны с прошлого обработанного фронта.
|
||||
voltage = (a - b, b - c, c - a) if self.connection == 'delta' else (a, b, c) # Преобразование Кларка само вычитает плавающий потенциал нейтрали звезды.
|
||||
self.motor.advance((stamp - start) / self.rate, voltage) # Точное решение учитывает длительность каждого фактического состояния ключей.
|
||||
self.sample = stamp # Обновление временной отметки предшествует приёму новых команд того же отсчёта.
|
||||
if pins is not None: # Периодические точки графика могут приходить без нового фронта.
|
||||
self.set_gates(pins) # Новые уровни действуют только справа от своей временной отметки.
|
||||
elif self.pins is not None: # Переоценка диодов на сетке ограничивает ошибку около смены знака тока.
|
||||
self.set_gates(tuple(None if value is None else int(value == self.active_high) for value in self.pins)) # Логические уровни переводятся обратно во входную полярность без двойной инверсии.
|
||||
return None if start is None or stamp == start else (start, stamp, previous, self.currents()) # Сегмент содержит физические амперы на обоих концах интервала.
|
||||
@@ -19,6 +19,11 @@ class PwmChannel:
|
||||
self.window_index = 1
|
||||
self.high = 0
|
||||
self.previous = None
|
||||
self.carrier_start = self.carrier_period = None # Два действительных активных фронта нужны для измерения несущей; начальный уровень не считается фронтом.
|
||||
self.carrier_count = self.carrier_samples = 0 # Счётчик полных периодов и их суммарная длина в отсчётах позволяют усреднять несущую без хранения записи.
|
||||
self.field_previous = self.field_crossing = self.field_pending = None # Интерполированное пересечение уровня 50% отделено от подтверждённого полного периода поля.
|
||||
self.field_armed = False # До отрицательной полуволны начальный фрагмент огибающей не может открыть измерение поля.
|
||||
self.field_carrier = None # Снимок счётчиков несущей привязывает среднюю частоту к последнему завершённому периоду поля.
|
||||
|
||||
def next_boundary(self):
|
||||
if self.window is None or self.origin is None:
|
||||
@@ -34,9 +39,33 @@ class PwmChannel:
|
||||
alpha = -math.expm1(-(midpoint - previous[0]) / self.tau)
|
||||
value = previous[1] + alpha * (value - previous[1])
|
||||
self.previous = (midpoint, value)
|
||||
carrier = self.rate / self.carrier_period if self.carrier_period and end - self.carrier_start <= 2 * self.carrier_period else None # После двух периодов без фронтов прежняя несущая больше не считается текущим измерением.
|
||||
field = self._field(midpoint, value) # Частота поля определяется по той же нормированной огибающей, которая выводится на график.
|
||||
return dict(start=start, end=end, duty=duty, value=value,
|
||||
frequency=self.rate / (end - start), midpoint=midpoint,
|
||||
previous=previous)
|
||||
previous=previous, carrier_frequency=carrier, field=field) # Частота окна остаётся совместимой со старым контрактом, измеренная несущая и полный период поля передаются отдельно.
|
||||
|
||||
def _field(self, sample, value): # Потоковый детектор измеряет электрическую частоту между одноимёнными пересечениями уровня заполнения 50%.
|
||||
previous = self.field_previous # Предыдущая точка нужна для линейной интерполяции времени пересечения между серединами периодов ШИМ.
|
||||
self.field_previous = (sample, value) # Храним только последнюю точку, поэтому память не зависит от длительности захвата.
|
||||
if value <= -0.02: # Гистерезис 0,02 нормированной шкалы соответствует одному процентному пункту заполнения ниже 50%.
|
||||
self.field_armed = True # Отрицательная полуволна разрешает одно следующее положительное пересечение.
|
||||
if self.field_armed and previous is not None and previous[1] <= 0 < value: # Учитываем только восходящий переход, чтобы не удвоить электрическую частоту.
|
||||
self.field_pending = previous[0] - previous[1] * (sample - previous[0]) / (value - previous[1]) # Интерполяция устраняет квантование периода поля шагом несущей.
|
||||
if value <= 0: # Возврат через нуль до положительного порога означает неподтверждённое пересечение.
|
||||
self.field_pending = None # Колебания внутри полосы гистерезиса не создают отдельные периоды.
|
||||
if not self.field_armed or self.field_pending is None or value < 0.02: # Положительный порог также отстоит от 50% на один процентный пункт заполнения.
|
||||
return None # Постоянное заполнение и неполные полуволны не получают выдуманную частоту поля.
|
||||
crossing = self.field_pending # Подтверждение использует время нулевого пересечения, а не более позднее достижение порога.
|
||||
last = self.field_crossing # Два подтверждённых восходящих пересечения ограничивают один полный электрический период.
|
||||
counters = (self.carrier_count, self.carrier_samples) # Снимок относится только к действительно наблюдавшимся активным фронтам.
|
||||
old_counters = self.field_carrier # Разность накопленных счётчиков даёт среднюю несущую за текущий период поля.
|
||||
self.field_crossing, self.field_carrier = crossing, counters # Следующий период начинается с текущего подтверждённого пересечения.
|
||||
self.field_armed, self.field_pending = False, None # Новое измерение разрешается только после следующей отрицательной полуволны.
|
||||
if last is None: # Первое пересечение задаёт начало, но ещё не содержит полного периода.
|
||||
return None # Начальный неполный фрагмент записи исключён из результата.
|
||||
count, elapsed = counters[0] - old_counters[0], counters[1] - old_counters[1] # Длительность измеренных периодов несущей выражена в отсчётах, а не миллисекундах.
|
||||
return dict(start=round(last), end=round(crossing), frequency=self.rate / (crossing - last), carrier_frequency=self.rate * count / elapsed if elapsed else None) # Полный период поля получает частоту в Гц и среднюю измеренную несущую, если за интервал были фронты.
|
||||
|
||||
def update(self, sample, pin):
|
||||
level = bool(pin) == self.active_high
|
||||
@@ -48,6 +77,8 @@ class PwmChannel:
|
||||
if sample < self.last:
|
||||
raise ValueError('Samples must be monotonic.')
|
||||
events = []
|
||||
if level != self.level and level and self.window is None: # В автоматическом режиме новый активный фронт одновременно завершает текущий период ШИМ.
|
||||
self._carrier_edge(sample) # Обновляем измерение до выдачи завершённого периода, чтобы частота относилась именно к нему.
|
||||
if self.window is not None:
|
||||
boundary = self.next_boundary()
|
||||
while boundary <= sample:
|
||||
@@ -58,6 +89,8 @@ class PwmChannel:
|
||||
self.window_index += 1
|
||||
boundary = self.next_boundary()
|
||||
self.high += (sample - self.last) * self.level
|
||||
if level != self.level and level: # В режиме окон частота несущей всё равно измеряется по фронтам, независимо от заданного размера окна.
|
||||
self._carrier_edge(sample) # Фронт на границе окна относится к следующему окну и не меняет уже выданное измерение.
|
||||
elif level != self.level:
|
||||
if level:
|
||||
if self.start is not None and self.fall is not None:
|
||||
@@ -67,3 +100,121 @@ class PwmChannel:
|
||||
self.fall = sample
|
||||
self.last, self.level = sample, level
|
||||
return events
|
||||
|
||||
def _carrier_edge(self, sample): # Оба режима восстановления используют единый счётчик фактических периодов несущей.
|
||||
if self.carrier_start is not None and sample > self.carrier_start: # Первый фронт только задаёт начало; нулевой период не допускается.
|
||||
self.carrier_period = sample - self.carrier_start # Интервал активный фронт — активный фронт задаёт полный период ШИМ в отсчётах.
|
||||
self.carrier_count += 1 # Счётчик нужен для среднего числа полных периодов за электрический цикл.
|
||||
self.carrier_samples += self.carrier_period # Суммируем длительности, чтобы переменная несущая усреднялась как число периодов за время.
|
||||
self.carrier_start = sample # Следующее измерение начинается с текущего реального фронта.
|
||||
|
||||
|
||||
class NpcChannel(PwmChannel): # Один вход NPC объединяет четыре затвора S1…S4, расположенные сверху вниз в фазной стойке.
|
||||
STATES = {(1, 1, 0, 0): 1, (0, 1, 1, 0): 0, (0, 0, 1, 1): -1} # Допустимые состояния классического NPC дают +Udc/2, нейтраль и −Udc/2 соответственно.
|
||||
|
||||
def __init__(self, samplerate, active_high=True, carrier_hz=0, smoothing_ms=0): # Настройки времени и полярности совпадают с двухуровневым режимом.
|
||||
super().__init__(samplerate, active_high, carrier_hz, smoothing_ms) # Общий расчёт частоты поля и фильтра применяется к среднему знаковому уровню NPC.
|
||||
self.integral = self.known = 0 # Интеграл уровня выражен в отсчётах; known хранит длительность достоверных состояний.
|
||||
self.last_valid = self.pulse_sign = None # Пропуск dead-time не должен ошибочно создавать новый импульс той же полярности.
|
||||
|
||||
def state(self, gates): # Преобразование использует все четыре затвора, а не только два внешних ключа.
|
||||
return self.STATES.get(tuple(int(bool(pin) == self.active_high) for pin in gates)) # Неизвестное состояние, включая все выключенные ключи, возвращает None вместо выдуманного нуля напряжения.
|
||||
|
||||
def _accumulate(self, sample): # Между цифровыми событиями уровень стойки считается постоянным только при допустимой комбинации затворов.
|
||||
if self.level is not None: # Во время dead-time напряжение зависит от тока и диодов и не определяется одними управляющими сигналами.
|
||||
self.integral += (sample - self.last) * self.level # Знаковая площадь сохраняет отличие положительных, нулевых и отрицательных участков.
|
||||
self.known += sample - self.last # В среднем используются лишь участки, однозначно восстановленные по затворам.
|
||||
self.last = sample # Новый интервал начинается после полностью учтённой части времени.
|
||||
|
||||
def _npc_emit(self, start, end): # Среднее по допустимым состояниям является оценкой огибающей, а не измерением напряжения в dead-time.
|
||||
coverage = self.known / (end - start) # Доля известного времени позволяет отделить короткие коммутационные паузы от повреждённой записи.
|
||||
if coverage < 0.95: # При более чем 5% неизвестного времени в периоде оценка считается недостаточно определённой и пропускается.
|
||||
self.previous = self.field_previous = self.field_crossing = self.field_pending = self.field_carrier = None # Разрыв не должен соединяться графиком или измерением поля с предыдущим достоверным участком.
|
||||
self.field_armed = False # Новое поле должно заново подтвердить отрицательную полуволну.
|
||||
return [] # Неизвестный интервал не превращается в фиктивный уровень нуля.
|
||||
high = (self.integral / self.known + 1) * (end - start) / 2 # Общий _emit ожидает эквивалентную активную длительность; преобразование сохраняет средний уровень −1…+1.
|
||||
event = self._emit(start, end, high) # Общая обработка формирует середину периода, частоты и фильтрованное значение.
|
||||
event['valid_fraction'] = coverage # Точность определения уровня видна в числовой аннотации и экспорте.
|
||||
return [event] # Формат списка совместим с обновлением двухуровневого канала.
|
||||
|
||||
def update(self, sample, gates): # Одно событие получает атомарный снимок четырёх цифровых входов.
|
||||
level = self.state(gates) # Только три штатные комбинации имеют определённый нормированный уровень стойки.
|
||||
if self.last is None: # Начальный уровень не является обнаруженным фронтом несущей.
|
||||
self.last, self.level, self.last_valid = sample, level, level # Состояние начинается с текущего снимка без выдуманного предыдущего импульса.
|
||||
if self.window is not None: # В режиме заданной несущей интегрирование начинается с области декодирования.
|
||||
self.origin = self.start = sample # Границы окон совпадают с прежним контрактом PwmChannel.
|
||||
return [] # Начальная точка ещё не содержит полного периода.
|
||||
if sample < self.last: # Равные номера допустимы для одновременных фронтов разных каналов.
|
||||
raise ValueError('Samples must be monotonic.') # Отрицательные интервалы нельзя интегрировать или выдавать в DSView.
|
||||
entry = level in (-1, 1) and level != self.last_valid # Начало ненулевого импульса распознаётся с учётом последнего достоверного состояния до dead-time.
|
||||
events = [] # Результаты содержат лишь полные периоды или окна.
|
||||
if self.window is not None: # Фиксированные окна сохраняют 0/100% и работают при отсутствии переключений.
|
||||
boundary = self.next_boundary() # Граница рассчитывается от начала, поэтому дробный период не накапливает ошибку округления.
|
||||
while boundary <= sample: # Одно цифровое событие может завершить несколько окон постоянного уровня.
|
||||
self._accumulate(boundary) # Учёт предыдущего состояния заканчивается точно на границе окна.
|
||||
events.extend(self._npc_emit(self.start, boundary)) # Неизвестные участки проверяются до формирования результата.
|
||||
self.start = boundary # Следующее окно начинается без перекрытия с предыдущим.
|
||||
self.integral = self.known = 0 # Площадь и известное время относятся только к новому окну.
|
||||
self.window_index += 1 # Номер следующей границы задаёт её положение в общей шкале отсчётов.
|
||||
boundary = self.next_boundary() # Повторное вычисление сохраняет точность дробной частоты.
|
||||
self._accumulate(sample) # Оставшаяся часть предыдущего уровня учитывается до изменения затворов.
|
||||
if entry: # Частота несущей измеряется по началам последовательных ненулевых импульсов одинакового знака.
|
||||
if self.pulse_sign == level: # Переход положительной полуволны в отрицательную не является коротким периодом несущей.
|
||||
self._carrier_edge(sample) # Измеренный период не подменяется частотой заданного окна.
|
||||
if self.window is None and self.start is not None: # Автоматический режим завершает период на одноимённом фронте следующего импульса.
|
||||
events.extend(self._npc_emit(self.start, sample)) # Огибающая получается из интеграла всех трёх уровней за полный период.
|
||||
else: # Первое появление другой полярности заново синхронизирует несущую этой полуволны.
|
||||
self.carrier_start, self.carrier_period = sample, None # Интервал через смену знака не принимается за измеренную частоту.
|
||||
self.pulse_sign = level # Следующий ненулевой импульс сравнивается с текущей полярностью.
|
||||
if self.window is None: # В автоматическом режиме начало и интеграл обновляются только на опорных фронтах.
|
||||
self.start = sample # Начальный неполный фрагмент полуволны исключается.
|
||||
self.integral = self.known = 0 # Новый период не наследует площадь предыдущего.
|
||||
self.level = level # Новое состояние действует только после текущего цифрового события.
|
||||
if level is not None: # Неизвестная коммутационная пауза не стирает информацию о последнем допустимом уровне.
|
||||
self.last_valid = level # Это исключает ложные импульсы при кратком выключении и возврате к прежней комбинации.
|
||||
return events # Адаптер получает тот же поток результатов, что для обычного ШИМ.
|
||||
|
||||
|
||||
class PhaseAnalysis: # Согласование времён A/B/C отделено от конкретного числа затворов в фазе.
|
||||
def __init__(self, samplerate): # Частота нужна для контроля устаревших оценок фазового периода.
|
||||
self.rate = samplerate # Все временные координаты остаются в отсчётах захвата.
|
||||
self.points, self.fields = {}, {} # Для интерполяции хранятся две последние точки каждой огибающей и последний полный период поля.
|
||||
self.last_vector = None # Одинаковый общий момент времени не должен выдавать повторный угол.
|
||||
|
||||
def add(self, channel, event): # Каждый восстановленный период обновляет только свою фазу.
|
||||
points = self.points.setdefault(channel, []) # Отсутствующая фаза не получает нулевой или выдуманный сигнал.
|
||||
if event.get('previous', True) is None: # Ядро явно разрывает огибающую после недостаточно определённого окна NPC.
|
||||
points.clear() # Интерполяция не должна соединять новую точку с данными до неизвестного участка.
|
||||
self.fields.pop(channel, None) # Межфазные углы также должны заново дождаться полного достоверного периода.
|
||||
points.append((event['midpoint'], event['value'])) # Угол вычисляется по восстановленным средним уровням, а не по мгновенным коммутациям.
|
||||
del points[:-2] # Ограниченная история сохраняет постоянный расход памяти на длинных записях.
|
||||
if event['field'] is not None: # Межфазные углы требуют наблюдавшихся полных электрических периодов.
|
||||
field = event['field'] # Результат содержит последнее восходящее пересечение и частоту данной фазы.
|
||||
self.fields[channel] = (field['end'], self.rate / field['frequency']) # Период хранится в отсчётах для вычисления сдвига в градусах.
|
||||
if any(len(self.points.get(ch, [])) < 2 for ch in range(3)): # Пространственный вектор нельзя восстановить из неподключённых или ещё не заполненных A/B/C.
|
||||
return None # Начальный участок записи не получает ложного θ.
|
||||
sample = min(self.points[ch][-1][0] for ch in range(3)) # Общая точка не выходит за наблюдавшееся время ни одной фазы.
|
||||
if sample <= max(self.points[ch][0][0] for ch in range(3)) or self.last_vector is not None and sample <= self.last_vector: # Требуется общий интервал и новое время, без экстраполяции остановившейся фазы.
|
||||
return None # Несогласованные точки не смешиваются в один вектор.
|
||||
values = [] # Три значения будут относиться к одному и тому же моменту времени.
|
||||
for ch in range(3): # Порядок A/B/C определяет положительное направление вращения.
|
||||
left, right = self.points[ch] # Две соседние точки ограничивают достоверный интервал данной фазы.
|
||||
values.append(left[1] + (right[1] - left[1]) * (sample - left[0]) / (right[0] - left[0])) # Линейная интерполяция устраняет сдвиг, вызванный разными моментами фронтов PWM A/B/C.
|
||||
alpha = (2 * values[0] - values[1] - values[2]) / 3 # Полное преобразование Кларка удаляет синфазную составляющую, включая добавку SVPWM.
|
||||
beta = (values[1] - values[2]) / math.sqrt(3) # Ортогональная ось соответствует последовательности A, B, C.
|
||||
self.last_vector = sample # Данный момент времени считается обработанным даже при почти нулевом векторе.
|
||||
if math.hypot(alpha, beta) < 0.02: # Ниже 2% нормированного уровня направление вектора не считается определённым.
|
||||
return dict(sample=sample, theta=None) # Разрыв явно запрещает соединять графиком углы через нулевую амплитуду.
|
||||
return dict(sample=sample, theta=math.degrees(math.atan2(beta, alpha)) % 360) # θ выражен в электрических градусах 0…360 относительно положительной оси A.
|
||||
|
||||
def differences(self, sample): # Положительный результат A→B означает запаздывание фазы B относительно A.
|
||||
angles = {} # Отсутствующая пара не получает выдуманный сдвиг 120 градусов.
|
||||
for first, second in ((0, 1), (1, 2), (2, 0)): # Измеряются все три запрошенные пары независимо.
|
||||
if first not in self.fields or second not in self.fields: # Каждой фазе пары нужен хотя бы один полный измеренный период.
|
||||
continue # Две подключённые фазы всё ещё могут дать одну доступную пару.
|
||||
a, b = self.fields[first], self.fields[second] # Последние одноимённые пересечения задают фазовые часы двух огибающих.
|
||||
if any(sample < item[0] or sample - item[0] > 2 * item[1] for item in (a, b)): # После двух периодов без нового измерения прежний сдвиг становится устаревшим.
|
||||
continue # Остановившийся или пропавший вход не сохраняет старое числовое значение.
|
||||
angle = 360 * ((sample - a[0]) / a[1] - (sample - b[0]) / b[1]) # Разность фазовых часов выражает запаздывание второй огибающей в градусах.
|
||||
angles['ABC'[first] + '→' + 'ABC'[second]] = (angle + 180) % 360 - 180 # Знаковый диапазон −180…180 различает запаздывание и опережение, включая обратную последовательность фаз.
|
||||
return angles # Результаты рассчитываются из записи и не предполагают заранее правильную трёхфазную систему.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Потоковый CIC-декодер однобитного сигма-дельта модулятора без зависимостей от GUI.""" # Ядро одинаково работает в DSView и автономных анализаторах.
|
||||
|
||||
|
||||
class SigmaDeltaFilter: # Каскад интеграторов и гребенчатых звеньев реализует фильтры sinc первого, второго и третьего порядка.
|
||||
def __init__(self, decimation=256, order=3, invert=False): # Децимация задаётся числом тактов на результат; 256 и sinc3 являются начальными настройками анализатора.
|
||||
if isinstance(decimation, bool) or not isinstance(decimation, int) or not 1 <= decimation <= 65536: # Ограничение 65536 тактов удерживает максимальное усиление sinc3 в пределах 49 бит.
|
||||
raise ValueError('Decimation must be an integer from 1 to 65536.') # Дробные и неположительные окна не определяют допустимую децимацию.
|
||||
if isinstance(order, bool) or not isinstance(order, int) or order not in (1, 2, 3): # Число интеграторов должно совпадать с числом гребенчатых звеньев.
|
||||
raise ValueError('Filter order must be 1, 2 or 3.') # Неизвестный порядок нельзя незаметно заменять sinc3.
|
||||
self.decimation = decimation # Каждый результат появляется после этого числа принятых битов.
|
||||
self.order = order # Порядок также определяет длительность начального установления фильтра.
|
||||
self.invert = bool(invert) # Инверсия применяется к битам до фильтрации и меняет знак восстановленного сигнала.
|
||||
self.gain = decimation ** order # Усиление CIC при постоянном входе равно R в степени порядка при дифференциальной задержке один.
|
||||
self.mask = (1 << self.gain.bit_length()) - 1 # Модуль степени двойки строго больше усиления сохраняет точный результат при переполнениях интеграторов.
|
||||
self.integrators = [0] * order # Нулевое начальное состояние не предполагает неизвестную предысторию захвата.
|
||||
self.delays = [0] * order # Каждый гребенчатый каскад хранит свой предыдущий вход на пониженной частоте.
|
||||
self.phase = 0 # Счётчик внутри окна исключает накопление дробной ошибки децимации.
|
||||
self.blocks = 0 # Первые order минус один результатов относятся к установлению и не выдаются пользователю.
|
||||
|
||||
def feed(self, bit): # Принимается ровно один уровень DATA на выбранном фронте CLK.
|
||||
if bit not in (0, 1): # Служебные значения неподключённых каналов sigrok не являются цифровыми уровнями.
|
||||
raise ValueError('Sigma-delta data must be a logic level 0 or 1.') # Ошибка назначения входов не должна превращаться в правдоподобную амплитуду.
|
||||
value = int(bit) ^ self.invert # Униполярное представление позволяет нормировать результат без знакового переполнения.
|
||||
for stage in range(self.order): # Последовательно обновляем интеграторы на полной частоте модулятора.
|
||||
self.integrators[stage] = (self.integrators[stage] + value) & self.mask # Модульная арифметика ограничивает память независимо от длительности записи.
|
||||
value = self.integrators[stage] # Следующий интегратор получает уже обновлённый выход предыдущего.
|
||||
self.phase += 1 # Считаем только фактически захваченные тактовые фронты.
|
||||
if self.phase < self.decimation: # Между границами децимации выходной отсчёт ещё не готов.
|
||||
return None # Неполные окна не дополняются вымышленными битами.
|
||||
self.phase = 0 # Новый блок начинается со следующего реального бита.
|
||||
for stage in range(self.order): # Гребенчатые звенья выполняются только на выходной частоте.
|
||||
previous = value # Задержка сохраняет вход текущего звена, а не его разность.
|
||||
value = (value - self.delays[stage]) & self.mask # Разности устраняют накопление интеграторов, включая их модульные переполнения.
|
||||
self.delays[stage] = previous # Это состояние используется через R входных тактов.
|
||||
self.blocks = min(self.order, self.blocks + 1) # Счётчик установления насыщается и не растёт вместе с записью.
|
||||
if self.blocks < self.order: # Для sincN пропускаем первые N минус один выходных блоков с неизвестной предысторией.
|
||||
return None # Частично установившийся фильтр не выдаёт ложное смещение постоянного сигнала.
|
||||
code = 2 * value - self.gain # Биполярный код принимает диапазон от минус R**N до плюс R**N без привязки к конкретному АЦП.
|
||||
return {'code': code, 'gain': self.gain, 'value': code / self.gain, 'density': value / self.gain} # Нормированная амплитуда и фильтрованная плотность единиц сохраняют точный масштаб CIC.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,110 @@
|
||||
"""Тонкий Python-порт общей CAN-карты UKSVEP на STM32F103RC.""" # Форматы, GPIO и расчёт неисправностей принадлежат C99 templates.
|
||||
from __future__ import annotations # Аннотации остаются совместимыми с исходниками Windows 7.
|
||||
import ctypes as ct # Передаёт скаляры и буферы единственной нативной реализации.
|
||||
from dataclasses import dataclass # Представляет готовые результаты C без протокольных вычислений.
|
||||
from .native import get_native_protocol, NativeProtocolUnavailable # Использует штатный поиск и жизненный цикл общей DLL.
|
||||
|
||||
@dataclass(frozen=True) # Метаданные одинаковы для всех представлений GUI.
|
||||
class Field: # Значения читаются из package.h-совместимой таблицы C.
|
||||
address: int # Нулевой индекс регистра modbus[128].
|
||||
maximum: int # Верхняя граница ввода в исходных единицах.
|
||||
initial: int # Значение предпросмотра, которое ещё не принято от платы.
|
||||
flags: int # 1 — параметр, 2 — команды, 4 — эмулируемое состояние.
|
||||
name: str # Общая подпись UTF-8 из C-профиля.
|
||||
|
||||
@dataclass(frozen=True) # Пакет отделён от физического CAN-драйвера.
|
||||
class Frame: # Готовые байты нельзя изменять через поля виджета после отправки.
|
||||
can_id: int # Полный 29-битный ID без аппаратного бита IDE.
|
||||
data: bytes # Восемь байтов payload, построенных C.
|
||||
|
||||
@dataclass(frozen=True) # Снимок содержит только реально присутствующие слова.
|
||||
class Packet: # Источник пакета отделяет ответ ВЭП от команды управляющего узла.
|
||||
from_device: bool # Истина означает ID данных ВЭП, а не собственное эхо команд.
|
||||
values: tuple[tuple[int, int], ...] # Пары адрес/значение, разрешённые маской присутствия C.
|
||||
|
||||
class UksvepCan: # Каждая панель использует общее нативное ядро без собственного codec.
|
||||
def __init__(self): # Привязывает только дополнительные экспорты профиля UKSVEP.
|
||||
self.lib = get_native_protocol().lib # Загрузка DLL и общий ABI уже проверены штатным портом.
|
||||
signatures = { # Типы описывают ABI, а не байтовый порядок wire-протокола.
|
||||
"uksvep_can_id": ([ct.c_uint, ct.c_uint], ct.c_uint32), # Возвращает маршрут для физического адреса и роли.
|
||||
"uksvep_can_encode": ([ct.c_uint, ct.c_uint, ct.c_uint, ct.POINTER(ct.c_uint16), ct.c_size_t, ct.POINTER(ct.c_uint8), ct.c_size_t], ct.c_size_t), # Кодирует до трёх слов в буфер caller.
|
||||
"uksvep_can_decode": ([ct.c_uint, ct.c_uint32, ct.POINTER(ct.c_uint8), ct.c_size_t, ct.POINTER(ct.c_uint16), ct.c_size_t], ct.c_int), # Возвращает плоский массив пар из C.
|
||||
"uksvep_field_count": ([], ct.c_uint), # Позволяет получить все поля без таблицы дублей Python.
|
||||
"uksvep_field_info": ([ct.c_uint, ct.c_uint, ct.POINTER(ct.c_uint16), ct.c_size_t], ct.c_int), # Возвращает четыре скаляра метаданных.
|
||||
"uksvep_field_name": ([ct.c_uint], ct.c_char_p), # C владеет постоянной строкой UTF-8.
|
||||
"uksvep_input_name": ([ct.c_uint], ct.c_char_p), # Назначения GPIO остаются общими с профилем.
|
||||
"uksvep_command_name": ([ct.c_uint], ct.c_char_p), # Пустое имя обозначает резервный бит R127.
|
||||
"uksvep_evaluate_inputs": ([ct.c_uint16, ct.c_uint16, ct.c_uint16, ct.POINTER(ct.c_uint16), ct.c_size_t], None), # Рассчитывает R1/R2 для эмуляции по алгоритму C.
|
||||
} # Символы добавлены без изменения существующего ABI версии 1.
|
||||
try: # Старая DLL должна давать понятную ошибку вместо тихого Python fallback.
|
||||
for name, (arguments, result) in signatures.items(): # Каждый экспорт получает точную сигнатуру до первого вызова.
|
||||
function = getattr(self.lib, name) # Отсутствие профиля в старой DLL обнаруживается здесь.
|
||||
function.argtypes = arguments # ctypes не угадывает размер указателей и целых.
|
||||
function.restype = result # Сохраняет 32-битные ID и указатели строк на 64-битном хосте.
|
||||
except AttributeError as error: # Формат wire не реализуется заново в аварийной ветви.
|
||||
raise NativeProtocolUnavailable("В общей DLL нет профиля UKSVEP. Пересоберите setprotocol.dll из templates.") from error # Поясняет требуемое действие пользователю.
|
||||
|
||||
@staticmethod # Валидация Python-аргумента предотвращает обрезание ctypes до C-типа.
|
||||
def _device(value): # Адрес хранится целым числом из четырёх перемычек.
|
||||
if not isinstance(value, int) or not 0 <= value <= 15: # Нельзя молча адресовать соседний узел по младшим битам.
|
||||
raise ValueError("DeviceID ВЭП должен быть 0…15") # Ошибка возникает до передачи CAN.
|
||||
return value # Валидный скаляр далее проверяется также C-профилем.
|
||||
|
||||
def can_id(self, device, from_device=False): # Доверяет вычисление маршрута общему C-профилю.
|
||||
return int(self.lib.uksvep_can_id(self._device(device), bool(from_device))) # Python не повторяет базу и смещение идентификатора.
|
||||
|
||||
def fields(self, device=0): # Получает подтверждённые поля выбранного ВЭП.
|
||||
device = self._device(device) # Проверяет адрес до создания начальных значений Mode.
|
||||
result = [] # Список содержит только готовые метаданные ABI.
|
||||
for index in range(self.lib.uksvep_field_count()): # Размер карты также принадлежит C.
|
||||
raw = (ct.c_uint16 * 4)() # Caller владеет явно ограниченным выходным буфером.
|
||||
if not self.lib.uksvep_field_info(index, device, raw, len(raw)): # Проверяет результат вызова вместо чтения частично готового массива.
|
||||
raise ValueError("Не удалось получить карту UKSVEP") # Ошибка не заменяется выдуманными defaults.
|
||||
name = self.lib.uksvep_field_name(index).decode("utf-8") # Декодирует только строковое представление общей подписи.
|
||||
result.append(Field(*map(int, raw), name)) # Не меняет единицы и адреса, возвращённые C.
|
||||
return tuple(result) # Карта не изменяется виджетами во время работы.
|
||||
|
||||
def names(self, kind): # Читает общие назначения входов либо команд.
|
||||
function = self.lib.uksvep_input_name if kind == "inputs" else self.lib.uksvep_command_name # Выбор относится к категории отображения, а не к codec.
|
||||
return tuple(function(bit).decode("utf-8") for bit in range(16)) # Резервные позиции остаются пустыми строками.
|
||||
|
||||
def frames(self, device, address, values, from_device=False): # Делит длинный снимок на штатные вызовы трёхсловного C-кодировщика.
|
||||
device = self._device(device) # Неверный адрес не может быть обрезан ctypes.
|
||||
values = tuple(values) # Фиксирует входной снимок до первой физической записи.
|
||||
if not values or not isinstance(address, int) or not 0 <= address < 128 or len(values) > 128 - address: # Проверяет Python-диапазон до преобразования unsigned.
|
||||
raise ValueError("Выберите непустой диапазон R0…R127") # Ошибка обнаруживается до формирования первого пакета.
|
||||
if any(not isinstance(value, int) or not 0 <= value <= 65535 for value in values): # Не обрезает отрицательные или переполненные слова.
|
||||
raise ValueError("Значения ВЭП должны быть uint16: 0…65535") # Поле пользователя не должно превращаться в другое число на проводе.
|
||||
result = [] # Все байты создаются до передачи, чтобы не отправить половину неверного снимка.
|
||||
for offset in range(0, len(values), 3): # Ограничение трёх слов задано существующим историческим форматом.
|
||||
part = values[offset:offset + 3] # UI-порт только разбивает контейнер, не строит маску/endian.
|
||||
source = (ct.c_uint16 * len(part))(*part) # Преобразует значения в явно типизированный вход C.
|
||||
target = (ct.c_uint8 * 8)() # Payload classic CAN ограничен восемью байтами.
|
||||
size = self.lib.uksvep_can_encode(device, bool(from_device), address + offset, source, len(part), target, len(target)) # Все поля wire заполняет C99.
|
||||
if size != 8: # Валидный UKSVEP кадр обязан иметь полный DLC.
|
||||
raise ValueError("C-профиль отклонил регистры UKSVEP") # Ошибка не скрывается частичной передачей.
|
||||
result.append(Frame(self.can_id(device, from_device), bytes(target))) # Возвращает готовый транспортно-независимый кадр.
|
||||
return tuple(result) # Физический writer принадлежит существующему BusTab.
|
||||
|
||||
def decode(self, device, can_id, data): # Вызывает общий decoder с ограниченными буферами.
|
||||
device = self._device(device) # Проверяет адрес до входа в ctypes.
|
||||
if not isinstance(can_id, int) or not 0 <= can_id <= 0x1FFFFFFF: # Исключает знаковое переполнение и биты вне extended ID.
|
||||
return None # Такое событие не принадлежит допустимому CAN-профилю.
|
||||
data = bytes(data) # Фиксирует входной payload на время вызова C.
|
||||
source = (ct.c_uint8 * len(data)).from_buffer_copy(data) # Caller владеет памятью до завершения синхронного разбора.
|
||||
target = (ct.c_uint16 * 8)() # Восемь скаляров вмещают направление и три адресные пары.
|
||||
count = self.lib.uksvep_can_decode(device, can_id, source, len(data), target, len(target)) # C выбирает ID, DLC, маску и границы карты.
|
||||
if count < 0: # Повреждённый пакет выбранного ВЭП не считается чужим.
|
||||
raise ValueError("Повреждённый CAN-кадр UKSVEP: DLC, маска или адрес") # UI может увеличить счётчик отказов RX.
|
||||
if count == 0: # Пакет другого адреса не меняет снимок.
|
||||
return None # Общая шина продолжает обрабатывать его независимо.
|
||||
pairs = tuple((int(target[2 + index * 2]), int(target[3 + index * 2])) for index in range(count)) # Переводит плоский ABI в готовые пары без повторного endian-разбора.
|
||||
return Packet(bool(target[0]), pairs) # Направление не позволяет выдать эхо TX за ответ платы.
|
||||
|
||||
def evaluate(self, inputs, alarm_mask, error_mask): # Предоставляет явный расчёт состояния эмулируемого ВЭП.
|
||||
values = (inputs, alarm_mask, error_mask) # Передаёт только исходные слова C-алгоритму ReadEnteres.
|
||||
if any(not isinstance(value, int) or not 0 <= value <= 65535 for value in values): # Защищает параметры от неявного uint16 обрезания.
|
||||
raise ValueError("Входы и маски должны быть uint16") # Неверный ввод не превращается в правдоподобную аварию.
|
||||
result = (ct.c_uint16 * 2)() # Буфер содержит R1 и R2, вычисленные C.
|
||||
self.lib.uksvep_evaluate_inputs(*values, result, len(result)) # Python не повторяет особые случаи заряд/разряд/24 В.
|
||||
return tuple(map(int, result)) # Результат используется только в исходящем эмулируемом снимке.
|
||||
@@ -468,6 +468,11 @@ 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:
|
||||
"""Скорость шины CAN, кбит/с."""
|
||||
return "S%s" % kbps
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -44,3 +44,10 @@ CANDLE_G474_BOOT = { # Профиль воспроизводит CAN_BOOTLOADER
|
||||
"device_type": 7, "device": 15, "product": 0, "hardware": 0, # Идентичность ProtoCAN BOOT: Type 7, Device 15, Product 0 и аппаратная ревизия 0.
|
||||
"can_bitrate": 500_000, "uart_baud": 512_000, # Classic CAN работает на 500 кбит/с, USART1 PA9/PA10 — на 512000 бод, 8N1.
|
||||
} # Адреса и скорости относятся только к CANdle G474VET6 и не подменяют профиль USB BOOT G474RB.
|
||||
|
||||
UKSVEP_F103RC_BOOT = { # Профиль отдельного boot-проекта UKSVEP_ICE_22220.6 согласован с ports/stm32f103rc-uksvep/boot_profile.h.
|
||||
"app_base": 0x08008000, "app_size": 94 * 1024, # Начальные 32 КиБ заняты BOOT; приложение заканчивается перед существующей EEPROM-страницей 0x0801F800.
|
||||
"stack_base": 0x20000000, "stack_end": 0x2000C000, # DS5792 определяет для STM32F103RC 48 КиБ SRAM и восьмибайтовое выравнивание начального MSP.
|
||||
"device_type": 5, "product": 0x5606, "hardware": 1, # DeviceID задают активные нулём перемычки PA6/PA7/PC4/PC5; Product отличает ВЭП от других STM32.
|
||||
"can_bitrate": 250_000, "uart_baud": 115_200, # Загрузчик UKSVEP использует CAN PA11/PA12 и USART1 PA9 TX/PA10 RX на 115200 бод.
|
||||
} # Single-slot обновление сохраняет параметры; для запуска требуется постоянный manifest и CRC образа.
|
||||
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user