Author SHA1 Message Date
Andrey 26f9d62896 Согласовать SCI-ядро и жизненный цикл прошивки с UlitCo
Сохранить совместимый переход RAM-ядра, постоянный поток Qt,
точную диагностику обмена и журнал без повторов процентов.
Добавить проверку транспортной точки входа официальным TI compiler.
2026-11-13 19:23:41 +03:00
Andrey 4d101302be Добавить SCI и J-Link и обновить общие порты устройств
Подключить профили прошивки из JSON и подготовку Python для CCS/Keil.
Добавить SCI-программатор F28335 и поддержку STM32 через J-Link.
Обновить Linux-консоль, CAN485 и Modbus и проверки общих портов.
2026-11-13 17:07:20 +03:00
Andrey 07929c880a feat(set-protocol): добавь профиль CAN УКСВЭП 2026-11-12 15:53:12 +03:00
Andrey 4f7f6cdb87 Объединить инструменты устройств с актуальной основной веткой 2026-11-12 15:00:41 +03:00
Andrey 1fc88684d5 Расширить DSView осциллографом и декодерами двигателя и сигма-дельта 2026-11-12 15:00:41 +03:00
Andrey fa5ea166a9 Добавить шлюзы CAN/RS485 и Modbus и уточнить обработку CAN 2026-11-12 15:00:22 +03:00
Andrey 062f064c85 Сохранить сведения о проекте и совместимость сборки с Python 3.7 2026-11-12 15:00:06 +03:00
Andrey 7b1a16d6e4 Исправить сборку CCS и Keil на Python 3.7
Убрать несовместимый параметр missing_ok при очистке временных файлов.
Обеспечить повторное копирование архивов без параметра dirs_exist_ok.
Добавить проверки генерации заголовков и экспорта с API Python 3.7.
2026-11-10 15:13:35 +03:00
Andrey a640f98f74 Добавить непрерывные окна и цифровой триггер Scope 2026-11-09 02:51:28 +03:00
Andrey f0edb70dbe Добавить ядро цифрового Scope и адаптер декодеров DSView 2026-11-09 02:03:13 +03:00
Andrey 6d59c3812c Улучшить обработку пропусков RJ2 и сборку DSView 2026-11-09 00:58:14 +03:00
Andrey e144c3e86a Добавить общие блоки Zynq, Ethernet DSView и декодер ШИМ 2026-11-09 00:15:26 +03:00
Andrey fa2ab3cc65 Merge origin/ump-v4 into main
Combine Flash CAN updates, signal processing and HOPE pin fixes with current Modbus/GAS, STM32 ports and CCS release exports. Resolve protocol, documentation and Qt conflicts; deduplicate Android JNI sources and include gauge/IGBT sources in host builds.
2026-11-07 01:32:27 +03:00
Andrey 5d2831794d Merge origin/main: resolve ProtoCAN Boot conflicts and preserve legacy STM32 ports 2026-11-07 01:15:26 +03:00
Andrey a1b2835457 Добавить общий каталог сигналов и плавную анимацию кота
Добавить общую модель двухканальных сигналов и анимированных XY-сцен
для предпросмотра, экспорта CSV и подготовки синхронных каналов.
Построить округлый силуэт кота кривыми Безье, добавить овальные глаза,
загнутый хвост и плавную походку: 32 позы по 125 точек, всего 4000 пар.

Проверка: 71 тест каталога, диапазонов, CSV и управления генератором.
2026-11-06 23:48:41 +03:00
Andrey dc4b0ab71b Исправить распиновку входов IGBT платы HOPE 2026-11-06 23:08:18 +03:00
Andrey cf75c5c855 Добавить общую графику прибора DEIF для HDMI 2026-11-06 21:21:36 +03:00
Andrey 491fa11569 Добавить аппаратные импульсы IGBT для HOPE G474 2026-11-06 16:33:17 +03:00
Andrey 149d19f3f9 Добавить аппаратный IGBT v4 и автономный стенд DEIF
Реализовать наносекундное управление IGBT для HOPE STM32G474 и клиент обмена с проверкой версии и записанных параметров.
Расширить Android/JNI API DEIF, настройки и локальную симуляцию; добавить тесты и документацию.
2026-11-06 16:25:47 +03:00
Andrey 45a34db657 Исправить каталог выпуска прошивки TMS320F2812
Размещать основной SCI8-комплект F2812 в flash, включая BIN, OUT, HEX,
карты, manifest и SHA-256. Передавать выбранный путь в указатель
последней сборки и экспорт; сохранить раскладку остальных процессоров.

Добавить проверку раскладки, паспортов и экспорта и описать правило.
Проверка: пять тестов общей сборки и реальный postbuild текущего OUT;
побайтовая сверка BIN после переноса.
2026-11-06 16:20:37 +03:00
Andrey bf5f094249 Расширить DEIF и генератор сигналов, подключить исходники KDiff3
Добавить общую модель сигналов XY и адаптировать генератор к двум каналам.
Расширить Android-порт DEIF общим контроллером рабочей области и проверками.
Подключить официальный репозиторий KDiff3 вложенным подмодулем.
2026-10-29 23:32:03 +03:00
Andrey 41e24629ce Расширить DEIF, загрузку CAN и общую обработку сигналов
Добавить общее ядро DEIF, эмуляцию приборов и переносимые панели Python/Qt.
Расширить загрузчики F2812 и F28335, обновление Flash и профиль STM Invertor.
Добавить альтернативный МНК Eigen, модели неисправностей и диагностику сигналов.
Обновить справочники устройств, проекты CCS и регрессионные проверки.
2026-10-24 15:40:51 +03:00
Andrey e52ba797e4 feat(set-protocol): Добавь DEIF XDi, STM Invertor и порт HOPE G474
Объедини реализацию DEIF в C99-ядре, чтобы Python и Android
использовали один кодек и общие эталонные кадры.
Добавь две GAS-проекции единой карты STM Invertor и аппаратные
порты эмулятора и загрузчика для платы HOPE STM32G474VET6.

Проверки: сборка MSVC Debug, 24 теста CTest, 30 тестов Python
и 29 дополнительных проверок, синтаксис 16 файлов Python.
2026-10-24 15:39:59 +03:00
Andrey 83fb9b6fe8 Обнови спецификацию и таблицы единого Modbus 2026-10-23 16:15:37 +03:00
Andrey 41f5b879fc Объедини Modbus в один тип с функциями и количеством 1–4 2026-10-23 16:15:11 +03:00
Andrey f17c124f0e docs: Согласуй документацию ProtoCAN и инструментов сборки
Обнови спецификацию GAS 0x3/0x4, реестр устройств и описание загрузочных сервисов одновременно в Markdown, HTML и XLSX.

Свяжи новые аппаратные порты, глобальное время и сборку C2000/ПЛИС с корневыми инструкциями, чтобы проекты подключались по одному актуальному контракту.
2026-10-23 14:16:22 +03:00
Andrey 2da7e7b525 feat(global-time): Добавь общие часы и порт TMS320F28335
Вынеси программные часы, коррекцию по ProtoCAN RTCSETUP и атомарную метку логгера в переносимое ядро без глобального состояния.

Подключи eCAN-A и CPU Timer2 отдельным портом F28335; зафиксируй календарные границы, переполнение, задержку RX и защиту IRQ в хостовых тестах.
2026-10-23 14:16:21 +03:00
Andrey 62c0492bfd feat(firmware-build): Добавь сборку и паспорта проектов ПЛИС
Подключи ISE, Vivado и Quartus к общему итератору версии и HDL-паспорту. Проверяй новый bit/sof до создания manifest, SHA-256 и архива выпуска.

Добавь установку локальных команд ISE, инструкции переноса и регрессионные проверки успешных и прерванных сборок.
2026-10-23 14:16:21 +03:00
Andrey 5bf3167064 build(firmware-info): Объедини подготовку и выпуск образов C2000
Направь CCS precompile/postcompile в общий c2000_build, чтобы резервирование версии, проверка EEPROM, MAP, SCI8 и Flash выполнялись одним процессом перед экспортом.

Сохраняй варианты eeprom, flash, can и rs в одном проверенном комплекте с manifest и SHA-256. Ошибка проверки блокирует постоянный выпуск и копирование устаревших образов.
2026-10-23 14:16:20 +03:00
Andrey c6390ae1c9 feat(protocan-boot): Добавь порты MCU и сервисы загрузки
Обеспечь единый процесс обновления для STM32F103/F407/G474 и TMS320F2812/F28335: discovery, служебные ответы, BUSY, тайм-аут сессии и подтверждение приложения. Добавь самостоятельные проекты CCS, аппаратные хранилища и Python-клиент.

Совместимость: структуры конфигурации и порта расширены, поэтому потребители должны пересобраться и обнулить новые поля. Для C28x callbacks используют pcan_boot_byte_t; периодический pcan_boot_poll обслуживает discovery и настроенный тайм-аут.
2026-10-23 14:16:19 +03:00
Andrey 876b58517c feat(set-protocol): Раздели GAS и добавь пакетный сбор регистров
Сохрани адресацию отдельных регистров GAS 0x3 и выдели слотам тип 0x4 с маской Word0. Добавь единое C-ядро GAS_WATCH2, тонкие Python/JNI-порты и проверки общих эталонных байтов.

Совместимость: тип 0x4 больше не обслуживает Modbus Coil. Потребителям PCAN_MSG_MODBUS_COIL и ProtoCanMsgToModbusCoil нужно перейти на GAS_SLOT и отдельный API слотов; регистровый GAS остаётся на 0x3.
2026-10-23 14:14:48 +03:00
Andrey 6ecf057a86 feat(c2000-linker): Добавить помощник настройки линкера C2000
Добавить генерацию карт COFF для TMS320F28335 и TMS320F2812, проверку CMD/MAP и русскую инструкцию.

Проверки: 14 тестов, включая четыре реальные линковки TI для RAM и Flash.
2026-10-19 16:33:40 +03:00
Andrey d42f2e1622 Перенести итератор версии прошивки в общий генератор
Увеличивать версию при прямом подключении precompile из CCS и Keil.
Сохранять счётчик атомарно и оставлять ручной режим без включённого флага.
Добавить проверки итератора и обновить примеры подключения.
2026-10-19 12:02:07 +03:00
Andrey 0c4eab4615 Добавить общий конструктор CAN-кадров для Qt 2026-10-17 14:43:13 +03:00
Andrey 5af8a8e9c8 Добавить общую Qt-консоль с фильтрами и экспортом 2026-10-17 14:43:12 +03:00
Andrey f613101124 Перезапускать импульс светодиода при каждом приёме пакета 2026-10-17 14:43:12 +03:00
Andrey cf65439400 Добавить выпуск и экспорт Flash-образов C2000 2026-10-17 14:43:11 +03:00
Andrey a5e46d4627 Расширить обмен по шинам, CAN-адаптеры и обработку графиков
Добавить очереди операций PBUS/SBUS, общую консоль, настройки Candle и SLCAN, декодеры DSView и обработку плотных графиков. Обновить протокол Бальзам и проверки общих компонентов.
2026-10-17 14:36:14 +03:00
Andrey 95c8b8ac7d Поддержать компактные записи логгера УМП v4
Разрешить 12-словные записи в запросах RTU и CAN. Распознавать статус v4 и декодировать оставшиеся сигналы с сохранением поддержки архивов v2/v3.
2026-10-12 12:26:37 +03:00
Andrey 32e2fbedc0 Поддержать компактные записи логгера УМП v4
Разрешить 12-словные записи в запросах RTU и CAN. Распознавать статус v4 и декодировать оставшиеся сигналы с сохранением поддержки архивов v2/v3.
2026-10-12 12:25:52 +03:00
Andrey bbb4bc2b9d Уточнить обработку плотных трендов и восстановление сигналов 2026-10-12 00:48:15 +03:00
Andrey 20454ff7fa Добавить поддержку GAS 167 и УМП, улучшить обмен по CAN и последовательному порту 2026-10-11 21:23:21 +03:00
Andrey 3c0f6fd9e6 Добавить загрузку F28335 по ProtoCAN и сервисные команды клиента 2026-10-11 03:24:44 +03:00
Andrey f6d45b70d4 Добавить порты ProtoCAN для STM32 и F28335 с загрузкой A/B 2026-10-11 03:24:41 +03:00
Andrey 226c2ca599 chore(merge): объедини pysetterminal с main
Сохрани актуальный генератор firmware identity с экспортом всей папки сборки и временем UTC+3 при объединении веток.
2026-10-09 11:04:32 +03:00
Andrey f2956b54cd merge(deif): объедини Android-компоненты с main 2026-10-09 10:55:08 +03:00
Andrey eab8dc5e0a fix(firmware-build): support CCS artifact paths and bootstrap identity config 2026-10-08 17:57:04 +03:00
Andrey 131f306131 fix(firmware-info): используй UTC+3 в именах папок
Переведи только время имени архива в UTC+3, сохрани UTC в метаданных и заголовке. Проверь переход на следующий день и копирование текущей папки.
2026-10-08 17:05:04 +03:00
Andrey 4fc0ad4e14 feat(firmware-info): копируй папку текущей сборки целиком
Добавь каталог назначения последним аргументом postcompile CCS и Keil. Копируй только архив текущего запуска с сохранением имени папки, всех файлов и проверкой SHA-256. Обнови MD-команды и проверки выбора папки, вложенных файлов и ошибок экспорта.
2026-10-08 16:26:21 +03:00
Andrey da055c2787 feat(firmware-info): архивируй сборки по хешу и дате
Создай в postcompile CCS и Keil папку по commit исходников и UTC-времени precompile. Сохрани точные образы, manifest, SHA-256 и метаданные, сохраняя прежние выходные файлы. Опиши копирование каталогов в SETGUI и проверь разные даты, целостность и Git-метаданные.
2026-10-08 15:31:13 +03:00
Andrey 8fc2e1b5cf fix(firmware-info): подключи Keil к JSON-схеме версии
Добавь prepare/seal для Keil через общий identity.py, образец конфигурации и команды Before/After Build. Для новой схемы не требуется отдельная release-конфигурация CMD или SETGUI. Проверь HEX/BIN, SHA-256, аргументы и пути с пробелами.
2026-10-08 15:13:34 +03:00
Andrey b5b2d1ae37 feat(firmware-info): добавь pre/post-build для CCS и Keil
Подключи общий генератор версии и SCI8-конвертацию через templates. Сохрани метаданные и SHA-256 готового BIN, совместимость с Python 3.8 и прежний SETGUI preflight. Опиши настройку CCS и локальное копирование результатов.
2026-10-08 14:58:27 +03:00
Andrey 7dfa8bd94d Добавить общие компоненты DEIF для Android и интерактивные приборы
Реализовать SAM/DAM, циклическую передачу, heartbeat, привязки и диагностику. Перенести рисунок и управление MASTER/SLAVE из SETGUI, добавить полноэкранный режим и проверки совместимости кадров.
2026-10-04 23:32:49 +03:00
606 changed files with 84169 additions and 1841 deletions
+3
View File
@@ -27,3 +27,6 @@ Desktop.ini
# Generated native library (built before packaging)
python/protocan/native/
# Кэш pytest создаётся проверками автоматически и не входит в исходники общей библиотеки.
.pytest_cache/
+3
View File
@@ -0,0 +1,3 @@
[submodule "third_party/kdiff3"]
path = third_party/kdiff3
url = https://invent.kde.org/sdk/kdiff3.git
+1 -1
View File
@@ -84,7 +84,7 @@ STM32F103RC, STM32F103ZE и STM32G474CE объявляют поддержку з
возвращают `PNAND_ERROR_WRITE_PROTECTED`, пока плата и порт не доработаны.
Рабочий пример полноценного G474-порта находится в соседнем проекте
`candleLight_fw`: `src/device/device_g4.c` отвечает за clock/GPIO,
`CANdle_stm32` (candleLight): `src/device/device_g4.c` отвечает за clock/GPIO,
`src/can/m_can.c` — за FDCAN, а `include/config.h` — за привязку платы
STM32G474VET6. Эти файлы остаются в прошивке, поскольку зависят от STM32 HAL,
USB gs_usb и конкретной разводки адаптера; переносимая логика протоколов
+20 -2
View File
@@ -24,6 +24,18 @@ templates/
## Что лежит
### FPGA и DSView
| Блок | Назначение |
|---|---|
| [Блоки логического анализатора](hdl/logic-analyzer/README.md) | Аппаратный триггер, BRAM, AXI и кольцевой буфер DDR |
| [Осциллограф](hdl/scope-acquisition/README.md) | Синхронизация каналов ADC, триггер, буфер и выдача записи |
| [Ethernet в FPGA](hdl/logic-udp-ethernet/README.md) | RGMII, MDIO для RTL8211F, UDP-поток и буфер восьми входов |
| [HDMI-отрисовка](hdl/logic-hdmi-preview/README.md) | График восьми цифровых каналов из входов или потока |
| [Трёхфазный ШИМ](hdl/vfd-pwm/README.md) | DDS, рампа, комплементарные выходы и мёртвое время |
| [DSView Ethernet](tools/dsview/zynq-ethernet/README.md) | Нативное подключение TCP/UDP, запуск и остановка |
| [ZNET](c/zynq-net/README.md) | C99-кодек Ethernet-команд ввода-вывода |
### C
| Библиотека | Что делает | Зависимости | Что нужно от платформы |
@@ -40,9 +52,10 @@ templates/
| [`c/set-protocol/ports/stm32f407-devboard-v1`](c/set-protocol/ports/stm32f407-devboard-v1) | доступ SETGUI к Modbus-регистрам F407 через CAN485 DevBoard_V1 | `pcan_modbus_server`, STM32 HAL CAN | bxCAN FIFO0 и callbacks карты регистров |
| [`c/set-protocol/ports/stm32-bxcan`](c/set-protocol/ports/stm32-bxcan) | порт прикладного ProtoCAN для STM32, бывший SETCAN; сохранён API `PROTOCAN_*` | STM32 HAL CAN/RTC/TIM + общее ядро `pcan_id` | classic bxCAN; настройки платы предоставляет прошивка |
| [`c/set-protocol/ports/tms320f2812`](c/set-protocol/ports/tms320f2812) | SETProtocol v2 firmware service по segmented classic CAN | `set_protocol`, `set_can`, `set_firmware` | CAN TX, Flash erase/write/read, optional signature policy и reboot |
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | CAN TX, erase/write Flash, boot metadata, проверка образа и reboot |
| [`c/protocan-boot`](c/protocan-boot) | адресная прошивка по ProtoCAN: A/B-слоты, сессия, CRC32, verify и rollback-контракт | C99 | **STM32F103C8/F407VE/G474VE и TMS320F2812/F28335-порты в комплекте**; проекты STM32 подключены через templates, есть boot main и вход из приложения |
| [`c/rs485-boot`](c/rs485-boot) | прошивка по RS-485 в формате SETGUI v1: потоковый parser, CRC32 и resume | C99 | UART TX/RX, DE, Flash — **порты STM32F103 и STM32G474VET в комплекте** |
| [`c/rtc-service`](c/rtc-service) | RTC с резервированным backup-томом | `stdint.h` | доступ к RTC и backup-памяти — **порт K1921VK028 в комплекте** |
| [`c/global-time`](c/global-time) | программные часы 1 мс, ProtoCAN RTCSETUP, готовность платы и атомарная метка логгера | C99 | IRQ lock, CAN TX, такт 1 мс — **порт TMS320F28335 eCAN-A / CPU Timer2 в комплекте** |
| [`c/firmware-info`](c/firmware-info) | SemVer, build stamp и git build ID работающего образа | C99 | config-порты STM32F1/F4/G4 и К1921ВК028; упаковка для Modbus/SETGUI |
| [`c/parallel-nand`](c/parallel-nand) | parallel NAND x8: raw read/program/erase, профили Micron MT29F1G08 и Hynix HY27UF084G2M, bad-block marker и банковое окно GAS для SETGUI | C99, опционально `set-protocol/pcan_gas` | **порты TMS320F2812, STM32F103RC/ZE, STM32F407VE и STM32G474CEU6; запись требует управляемого WP#** |
@@ -62,8 +75,13 @@ templates/
### Инструменты
Исходники внешних приложений находятся в [`third_party`](third_party/README.md):
[`third_party/kdiff3`](third_party/kdiff3) содержит официальный репозиторий KDiff3
как вложенный Git submodule для развития сравнения файлов и каталогов в SETGUI.
| Инструмент | Что делает |
|---|---|
| [`tools/firmware-build/fpga`](tools/firmware-build/fpga/README.md) | общий итератор версии и HDL-паспорт для Xilinx ISE, Vivado и Altera/Intel Quartus; контроль `.bit`/`.sof`, manifest, SHA-256 и архив |
| [`tools/firmware-publish`](tools/firmware-publish) | единый BAT и конфигурации для проверки и публикации `.hex` Keil / `.bin` CCS 12 в каталоге SETGUI |
Общие тренды для Android GUI и SETGUI: [формат JSON, C99-ядро и адаптеры](c/set-protocol/docs/GUI_TRENDS.md).
@@ -118,7 +136,7 @@ git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git
| `KONOR_ds18b20` | st7789, keypad, menu, eeprom-ft24c256, can-sensor, ds18b20, firmware-info |
| `OpticalTester` | st7789, keypad, menu, eeprom-ft24c256 |
| `CAN_to_RS485` | set-protocol legacy transport, python/protocan |
| `candleLight_fw` | эталон разделения общей логики и G431/G474 FDCAN-порта; общий ProtoCAN Boot переносится по контракту `protocan-boot` |
| `CANdle_stm32` (candleLight) | G431/G474 FDCAN board-support; CAN/UART-загрузчик G474 и вход из приложения подключены к `protocan-boot/ports/stm32g474` через templates |
| `SETGUI` | python/protocan |
| `SETGUI`, Android GUI и новые устройства SET | set-protocol |
| `k1921vk028` | rtc-service |
+15
View File
@@ -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() # Завершает платформенную часть библиотеки.
+75
View File
@@ -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 не обновляет уже собранную прошивку автоматически.
+24
View File
@@ -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
+1 -1
View File
@@ -9,5 +9,5 @@ if(NOT WIN32)
endif()
add_library(candle SHARED candle.c candle_ctrl_req.c candle.def)
target_compile_definitions(candle PRIVATE UNICODE _UNICODE)
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32)
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32 cfgmgr32) # Configuration Manager сопоставляет интерфейсы CAN и DFU по родительскому USB-узлу.
target_include_directories(candle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+149 -4
View File
@@ -34,6 +34,86 @@
#include "candle_defs.h"
#include "candle_ctrl_req.h"
#include "ch_9.h"
#include <cfgmgr32.h> /* Сопоставляет интерфейсы CAN и DFU по общему родительскому USB-устройству Windows. */
static DWORD candle_dfu_parent(const wchar_t *path, DEVINST *parent) /* Возвращает родителя интерфейса, чтобы команда не попала в другой подключённый адаптер. */
{ /* Начало поиска физического USB-устройства по пути выбранного CAN-интерфейса. */
HDEVINFO devices = SetupDiCreateDeviceInfoList(NULL, NULL); /* Создаёт временный набор SetupAPI без перечисления посторонних устройств. */
if (devices == INVALID_HANDLE_VALUE) return GetLastError(); /* Сохраняет причину отказа создания набора Windows. */
SP_DEVICE_INTERFACE_DATA iface = {0}; /* Подготавливает описание интерфейса для открытия по точному пути. */
iface.cbSize = sizeof(iface); /* Передаёт SetupAPI размер структуры текущей разрядности процесса. */
SP_DEVINFO_DATA info = {0}; /* Получает идентификатор узла PnP выбранного интерфейса. */
info.cbSize = sizeof(info); /* Указывает размер структуры устройства согласно контракту SetupAPI. */
DWORD size = 0, error = ERROR_SUCCESS; /* Хранит требуемое число байтов и итоговый код Windows. */
if (!SetupDiOpenDeviceInterfaceW(devices, path, 0, &iface)) error = GetLastError(); /* Открывает только заданный путь, сохраняя ошибку исчезнувшего устройства. */
if (!error) SetupDiGetDeviceInterfaceDetailW(devices, &iface, NULL, 0, &size, NULL); /* Узнаёт объём буфера переменной длины для пути USB-интерфейса. */
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail = size ? malloc(size) : NULL; /* Выделяет буфер только после успешного определения его размера. */
if (!error && !detail) error = size ? ERROR_NOT_ENOUGH_MEMORY : GetLastError(); /* Отличает нехватку памяти от ошибки поиска описания. */
if (detail) { /* Обрабатывает описание только при наличии выделенного буфера. */
detail->cbSize = sizeof(*detail); /* Согласует размер заголовка с архитектурой Win32 или Win64. */
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, size, NULL, &info)) error = GetLastError(); /* Получает узел интерфейса из SetupAPI. */
else if (CM_Get_Parent(parent, info.DevInst, 0) != CR_SUCCESS) error = ERROR_NOT_FOUND; /* Получает общий родительский узел составного USB-адаптера. */
free(detail); /* Освобождает описание после получения идентификатора родителя. */
} /* Завершает работу с описанием интерфейса. */
SetupDiDestroyDeviceInfoList(devices); /* Освобождает набор SetupAPI при любом результате поиска. */
return error; /* Возвращает ноль при успехе либо причину отказа Windows. */
} /* Завершает сопоставление интерфейса с физическим USB-устройством. */
uint32_t __stdcall candle_dev_enter_dfu(const wchar_t *can_path, uint8_t status[6]) /* Возвращает код Windows и шестибайтовый DFU_GETSTATUS согласно DFU 1.1. */
{ /* Начало отдельного сеанса управляющего интерфейса обновления. */
if (!can_path || !status) return ERROR_INVALID_PARAMETER; /* Запрещает отсутствие точного пути адаптера или выходного буфера статуса. */
memset(status, 0, 6); /* Исключает использование неинициализированных полей ответа при ошибке USB. */
DEVINST wanted_parent = 0; /* Запоминает физический адаптер, выбранный пользователем до остановки CAN. */
DWORD error = candle_dfu_parent(can_path, &wanted_parent); /* Ищет родителя по сохранённому пути CAN, не заменяя его первым найденным устройством. */
if (error) return error; /* Прерывает переход, если выбранный адаптер уже исчез из PnP. */
GUID guid = {0xc25b4308, 0x04d3, 0x11e6, {0xb3, 0xea, 0x60, 0x57, 0x18, 0x9e, 0x64, 0x43}}; /* GUID интерфейса обновления из дескриптора CANable 2.5. */
HDEVINFO devices = SetupDiGetClassDevsW(&guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); /* Перечисляет только подключённые интерфейсы обновления CANable 2.5. */
if (devices == INVALID_HANDLE_VALUE) return GetLastError(); /* Возвращает отказ SetupAPI без попытки послать команду. */
error = ERROR_NOT_FOUND; /* Отсутствие интерфейса обновления не считается успешным входом в DFU. */
for (DWORD index = 0; ; ++index) { /* Проверяет интерфейсы до точного совпадения физического устройства. */
SP_DEVICE_INTERFACE_DATA iface = {0}; /* Создаёт независимое описание очередного DFU-интерфейса. */
iface.cbSize = sizeof(iface); /* Задаёт обязательный размер структуры SetupAPI. */
if (!SetupDiEnumDeviceInterfaces(devices, NULL, &guid, index, &iface)) { /* Завершает обход либо сообщает ошибку перечисления. */
if (GetLastError() != ERROR_NO_MORE_ITEMS) error = GetLastError(); /* Отличает штатный конец списка от отказа Windows. */
break; /* Выходит из обхода после исчерпания интерфейсов или ошибки. */
} /* Завершает проверку результата перечисления. */
DWORD size = 0; /* Хранит размер описания очередного интерфейса в байтах. */
SetupDiGetDeviceInterfaceDetailW(devices, &iface, NULL, 0, &size, NULL); /* Определяет буфер для полного пути интерфейса обновления. */
if (!size) { error = GetLastError(); break; } /* Не продолжает работу с неполным результатом SetupAPI. */
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail = malloc(size); /* Выделяет описание с достаточным местом для USB-пути. */
if (!detail) { error = ERROR_NOT_ENOUGH_MEMORY; break; } /* Не допускает обращения к отсутствующему буферу. */
detail->cbSize = sizeof(*detail); /* Сохраняет ABI описания интерфейса Windows. */
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, size, NULL, NULL)) { /* Получает путь, который можно передать CreateFileW. */
error = GetLastError(); free(detail); break; /* Сохраняет ошибку и освобождает буфер перед выходом. */
} /* Завершает чтение пути интерфейса. */
DEVINST parent = 0; /* Хранит родителя текущего кандидата на обновление. */
if (candle_dfu_parent(detail->DevicePath, &parent) || parent != wanted_parent) { /* Исключает каждый интерфейс другого физического адаптера. */
free(detail); continue; /* Освобождает описание неподходящего кандидата и продолжает поиск. */
} /* Завершает проверку принадлежности выбранному USB-адаптеру. */
HANDLE file = CreateFileW(detail->DevicePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); /* Эксклюзивно открывает интерфейс обновления выбранного устройства для WinUSB. */
free(detail); /* Путь больше не нужен после открытия системного дескриптора. */
if (file == INVALID_HANDLE_VALUE) { error = GetLastError(); break; } /* Не скрывает отсутствие доступа или занятость интерфейса. */
WINUSB_INTERFACE_HANDLE usb = NULL; /* Хранит отдельный WinUSB-сеанс интерфейса обновления без CAN bulk endpoint. */
error = ERROR_SUCCESS; /* Сбрасывает исходное отсутствие интерфейса после точного совпадения устройства. */
if (!WinUsb_Initialize(file, &usb)) error = GetLastError(); /* Проверяет наличие рабочего WinUSB-драйвера DFU runtime. */
USB_INTERFACE_DESCRIPTOR descriptor = {0}; /* Получает реальный номер и класс открытого интерфейса. */
if (!error && !WinUsb_QueryInterfaceSettings(usb, 0, &descriptor)) error = GetLastError(); /* Не предполагает, что произвольный WinUSB-интерфейс поддерживает DFU. */
if (!error && (descriptor.bInterfaceClass != 0xFE || descriptor.bInterfaceSubClass != 1 || descriptor.bInterfaceProtocol != 1)) error = ERROR_NOT_SUPPORTED; /* Проверяет класс приложения, подкласс DFU и runtime-протокол по DFU 1.1. */
ULONG timeout = 500, transferred = 0; /* Ограничивает управляющий USB-запрос 500 миллисекундами и учитывает длину ответа. */
if (!error && !WinUsb_SetPipePolicy(usb, 0, PIPE_TRANSFER_TIMEOUT, sizeof(timeout), &timeout)) error = GetLastError(); /* Устанавливает тайм-аут endpoint 0, чтобы отсутствующий ответ не блокировал программу. */
WINUSB_SETUP_PACKET packet = {0}; /* Обнуляет поля USB SETUP перед запросом перехода в загрузчик. */
packet.RequestType = 0x21; packet.Request = 0; packet.Value = 1000; packet.Index = descriptor.bInterfaceNumber; /* DFU_DETACH: class/interface OUT, запрос 0, ожидаемое время отключения 1000 мс. */
if (!error && !WinUsb_ControlTransfer(usb, packet, NULL, 0, &transferred, NULL)) error = GetLastError(); /* Отправляет DETACH и требует подтверждённой USB-транзакции. */
packet.RequestType = 0xA1; packet.Request = 3; packet.Value = 0; packet.Length = 6; /* DFU_GETSTATUS: class/interface IN, запрос 3, ответ ровно 6 байт по DFU 1.1. */
if (!error && !WinUsb_ControlTransfer(usb, packet, status, 6, &transferred, NULL)) error = GetLastError(); /* Проверяет результат до отложенного перехода прошивки через 300 мс. */
if (!error && transferred != 6) error = ERROR_INVALID_DATA; /* Не трактует укороченный ответ как подтверждение входа в загрузчик. */
if (usb) WinUsb_Free(usb); /* Освобождает WinUSB до появления загрузчика с другим VID/PID. */
CloseHandle(file); /* Освобождает файловый дескриптор после успеха и при любой ошибке USB. */
break; /* Не отправляет DETACH повторно и не переключается на другой адаптер при отказе. */
} /* Завершает поиск и обработку единственного выбранного адаптера. */
SetupDiDestroyDeviceInfoList(devices); /* Освобождает перечень интерфейсов при любом результате. */
return error; /* Статус прошивки возвращается отдельно от системной ошибки USB. */
} /* Завершает управление DFU runtime без записи flash или изменения образа. */
static bool candle_dev_interal_open(candle_handle hdev);
@@ -656,6 +736,30 @@ bool __stdcall DLL candle_dev_open(candle_handle hdev)
}
bool __stdcall DLL candle_dev_get_product(candle_handle hdev, wchar_t *product, uint32_t capacity) /* Читает USB Product String открытого адаптера в буфер UTF-16; capacity измеряется в wchar_t, включая завершающий ноль. */
{ /* Начало чтения отображаемого имени без изменения режима CAN. */
if (!product || !capacity) return false; /* Не пишет в отсутствующий либо пустой выходной буфер. */
product[0] = 0; /* Сохраняет пустую строку при любом отказе чтения дескриптора. */
candle_device_t *dev = (candle_device_t*)hdev; /* Получает уже открытый WinUSB-сеанс выбранного физического адаптера. */
if (!dev || !dev->winUSBHandle) return false; /* Не открывает устройство заново и не обращается к закрытому интерфейсу. */
USB_DEVICE_DESCRIPTOR descriptor = {0}; /* Получает индекс строки продукта из стандартного дескриптора устройства. */
ULONG received = 0; /* Хранит фактически полученное количество байтов управляющего ответа. */
if (!WinUsb_GetDescriptor(dev->winUSBHandle, 1, 0, 0, (PUCHAR)&descriptor, sizeof(descriptor), &received) || received != sizeof(descriptor)) return false; /* Тип 1 обозначает Device Descriptor по USB 2.0; неполный ответ не используется. */
UCHAR string_index = descriptor.iProduct; /* Для обычных адаптеров использует объявленный устройством индекс USB Product String. */
if (descriptor.idVendor == 0x1D50 && descriptor.idProduct == 0x606F) string_index = 2; /* В CANable Candlelight имя продукта имеет индекс 2; WinUSB может подменять iProduct индексом имени интерфейса, как описано в штатном примере проекта. */
if (!string_index) return false; /* Нулевой iProduct означает, что устройство не предоставляет имя продукта. */
UCHAR raw[256] = {0}; /* Буфер вмещает строковый дескриптор USB, чей однобайтовый bLength ограничен 255 байтами. */
USHORT language = 0x0409; /* Использует английский идентификатор USB как резервный вариант для устройств без списка языков. */
if (WinUsb_GetDescriptor(dev->winUSBHandle, 3, 0, 0, raw, sizeof(raw), &received) && received >= 4 && raw[0] >= 4 && raw[1] == 3) language = (USHORT)(raw[2] | ((USHORT)raw[3] << 8)); /* Тип 3 и индекс 0 возвращают список LANGID; выбирает первый язык, предоставленный адаптером. */
if (!WinUsb_GetDescriptor(dev->winUSBHandle, 3, string_index, language, raw, sizeof(raw), &received)) return false; /* Читает именно строку продукта на поддерживаемом устройством языке. */
if (received < 2 || raw[1] != 3 || raw[0] < 2 || raw[0] > received || (raw[0] & 1)) return false; /* Проверяет тип, размер и чётное число байтов UTF-16 перед копированием в выходной буфер. */
uint32_t count = (raw[0] - 2) / 2; /* Исключает двухбайтовый заголовок и переводит длину UTF-16LE из байтов в число кодовых единиц. */
if (count >= capacity) return false; /* Не обрезает индивидуальный номер в конце имени и оставляет место для завершающего нуля. */
for (uint32_t index = 0; index < count; ++index) product[index] = (wchar_t)(raw[2 + index * 2] | ((USHORT)raw[3 + index * 2] << 8)); /* Явно собирает кодовые единицы UTF-16LE без предположений о выравнивании массива байтов. */
product[count] = 0; /* Завершает строку для безопасного чтения через ctypes и отображения в Qt. */
return count > 0; /* Пустое имя оставляет интерфейсу стандартную резервную подпись адаптера. */
} /* Завершает получение настоящего имени из прошивки USB-адаптера. */
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us)
{
return candle_ctrl_get_timestamp(hdev, timestamp_us);
@@ -823,6 +927,38 @@ bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch)
return candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_RESET, 0);
}
bool __stdcall DLL candle_channel_start_configured(candle_handle hdev, uint8_t ch, uint32_t flags) /* Новый вход сохраняет прежний ABI start и позволяет отключать HW_TIMESTAMP. */
{ /* Явно выбранные Python-флаги должны попасть в прошивку без автоматического дополнения. */
return candle_ctrl_set_device_mode((candle_device_t*)hdev, ch, CANDLE_DEVMODE_START, flags); /* Передаём точную маску после проверки возможностей на уровне транспорта. */
} /* Завершение запуска с явно заданными параметрами. */
bool __stdcall DLL candle_channel_get_data_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap) /* Экспортируем чтение FD-пределов для расчёта тайминга на любой частоте CAN. */
{ /* Общая форма capability позволяет повторно использовать валидатор номинального тайминга. */
return candle_ctrl_data_capability((candle_device_t*)hdev, ch, cap); /* USB-запрос возвращает реальные пределы выбранного канала. */
} /* Завершение получения возможностей быстрой фазы. */
bool __stdcall DLL candle_channel_set_termination(candle_handle hdev, uint8_t ch, uint32_t state) /* Значения 0 и 1 соответствуют OFF и ON стандарта gs_usb. */
{ /* Проверка исключает отправку неопределённого состояния в прошивку. */
return state <= 1 && candle_ctrl_termination((candle_device_t*)hdev, ch, &state, true); /* Выполняем запись только для допустимого состояния. */
} /* Завершение переключения терминатора. */
bool __stdcall DLL candle_channel_get_termination(candle_handle hdev, uint8_t ch, uint32_t *state) /* Чтение состояния не меняет топологию CAN. */
{ /* Используем тот же формат 32-битного значения, что и при записи. */
return candle_ctrl_termination((candle_device_t*)hdev, ch, state, false); /* false выбирает USB_DIR_IN и GET_TERMINATION. */
} /* Завершение чтения терминатора. */
bool __stdcall DLL candle_channel_get_diagnostics(candle_handle hdev, uint8_t ch, uint32_t *state, uint32_t *rxerr, uint32_t *txerr) /* Сохраняем счётчики, которые старый get_state отбрасывал. */
{ /* Диагностика выполняется только для уже открытого устройства. */
candle_device_state_t data = {0}; /* Полная структура протокола содержит три слова: состояние, RX и TX. */
if (!candle_ctrl_get_state((candle_device_t*)hdev, ch, &data)) { /* Не возвращаем нулевые счётчики как будто запрос был успешным. */
return false; /* Причина USB-отказа уже записана управляющим запросом. */
} /* Дальнейшее копирование относится только к подтверждённому ответу. */
*state = data.state; /* Сохраняем состояние Error-active, Warning, Passive, Bus-off, Stopped или Sleeping. */
*rxerr = data.rxerr; /* Счётчик ошибок приёма измеряется в единицах CAN-контроллера. */
*txerr = data.txerr; /* Счётчик ошибок передачи помогает отличать отсутствие ACK от ошибок приёма. */
return true; /* Все три выходных значения относятся к одному USB-ответу. */
} /* Завершение расширенной диагностики канала. */
/* Write len bytes from buf to the OUT pipe, aborting after 300 ms.
* Writes are serialised by writeMutex in CandleApiInterface so dev->txEvent
* is never accessed by two threads simultaneously. */
@@ -1017,14 +1153,13 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
}
dev->rxurbs[urb_num].pending = false;
/* Minimum: classic CAN header (12 bytes) + at least 8 data bytes = 20 bytes */
static const DWORD classic_min = sizeof(candle_frame_t) - 4;
if (bytes_transfered < classic_min) {
const DWORD header_size = 12; /* Заголовок gs_usb занимает 12 байт в Classic и FD; короткий FD может не содержать восьми байтов данных. */
if (bytes_transfered < header_size) { /* До чтения flags и DLC проверяем наличие полного заголовка. */
candle_prepare_read(dev, urb_num);
candle_logf(L"fd read too small urb=%u bytes=%lu min=%lu",
urb_num,
bytes_transfered,
classic_min);
header_size); /* Минимальная длина здесь относится к заголовку, а payload проверяется после определения формата. */
dev->last_error = CANDLE_ERR_READ_SIZE;
return false;
}
@@ -1047,6 +1182,11 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
if (is_fd_frame) {
/* can_dlc is at byte offset 8 in both classic and FD wire frames. */
const uint8_t raw_dlc = dev->rxurbs[urb_num].buf[8];
if (raw_dlc > 15) { /* FD DLC является четырёхбитным кодом и не может содержать значения выше 15. */
candle_prepare_read(dev, urb_num); /* Повреждённый USB-пакет не должен останавливать последующий приём. */
dev->last_error = CANDLE_ERR_READ_SIZE; /* Неверный DLC считается ошибкой формата входного кадра. */
return false; /* Не превращаем повреждённый DLC в пустой успешный FD-кадр. */
} /* Далее длина безопасно вычисляется по таблице CAN FD. */
const DWORD data_len = candle_dlc_to_len(raw_dlc);
const DWORD min_size = 12 + data_len; /* header + data, without timestamp */
@@ -1077,6 +1217,11 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
}
/* else: timestamp stays zero from memset above */
} else {
if (bytes_transfered < sizeof(candle_frame_t) - sizeof(uint32_t)) { /* Classic-пакет обязан содержать весь восьмибайтовый буфер данных, даже при меньшем DLC. */
candle_prepare_read(dev, urb_num); /* Возобновляем соответствующий URB после отклонения неполного пакета. */
dev->last_error = CANDLE_ERR_READ_SIZE; /* Не допускаем чтение неполученных данных из локальной структуры. */
return false; /* Повреждённый Classic-пакет не попадает в Python через FD API. */
} /* Минимальный размер Classic теперь проверен отдельно от коротких FD. */
/* Classic CAN frame — copy into FD struct, fixing the timestamp position */
candle_frame_t classic;
DWORD copy_len = (bytes_transfered < sizeof(classic)) ? bytes_transfered : sizeof(classic);
+12
View File
@@ -3,6 +3,8 @@
; прикладные регистры SETProtocol.
EXPORTS
candle_dev_get_product ; Имя продукта из USB-дескриптора прошивки для списка адаптеров SETGUI.
candle_dev_enter_dfu ; Переход через отдельный DFU runtime-интерфейс того же физического USB-адаптера.
candle_list_scan
candle_list_free
candle_list_length
@@ -31,3 +33,13 @@ EXPORTS
candle_frame_data
candle_frame_timestamp_us
candle_dev_last_error
candle_channel_start_configured ; Точная маска запуска с управляемыми аппаратными метками.
candle_channel_get_data_capabilities ; Пределы тайминга быстрой фазы CAN FD.
candle_channel_set_termination ; Переключение встроенного терминатора.
candle_channel_get_termination ; Чтение состояния встроенного терминатора.
candle_channel_get_diagnostics ; Состояние и счётчики ошибок CAN.
candle_channel_set_data_timing ; Тайминг фазы данных CAN FD.
candle_fd_frame_send ; Отправка полного кадра CAN FD до 64 байт.
candle_fd_frame_read ; Приём FD и Classic без обрезания данных.
candle_fd_frame_type ; Различение RX, TX echo и ошибок FD.
candle_fd_frame_id ; Получение CAN ID из структуры FD.
+7
View File
@@ -246,7 +246,9 @@ bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len);
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev);
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state);
wchar_t * __stdcall DLL candle_dev_get_path(candle_handle hdev);
bool __stdcall DLL candle_dev_get_product(candle_handle hdev, wchar_t *product, uint32_t capacity); /* Возвращает USB Product String в UTF-16; capacity включает завершающий ноль и измеряется в wchar_t. */
bool __stdcall DLL candle_dev_open(candle_handle hdev);
uint32_t __stdcall DLL candle_dev_enter_dfu(const wchar_t *can_path, uint8_t status[6]); /* Переводит выбранный физический адаптер в DFU и возвращает Win32-код плюс ответ DFU_GETSTATUS. */
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us);
bool __stdcall DLL candle_dev_close(candle_handle hdev);
bool __stdcall DLL candle_dev_free(candle_handle hdev);
@@ -259,6 +261,11 @@ bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, can
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate);
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags);
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch);
bool __stdcall DLL candle_channel_start_configured(candle_handle hdev, uint8_t ch, uint32_t flags); /* Запускаем с точной маской без принудительного включения аппаратных меток. */
bool __stdcall DLL candle_channel_get_data_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap); /* Возвращаем пределы тайминга быстрой фазы FD. */
bool __stdcall DLL candle_channel_set_termination(candle_handle hdev, uint8_t ch, uint32_t state); /* Состояние терминатора: 0 — выключен, 1 — включён. */
bool __stdcall DLL candle_channel_get_termination(candle_handle hdev, uint8_t ch, uint32_t *state); /* Чтение не меняет подключение встроенного резистора. */
bool __stdcall DLL candle_channel_get_diagnostics(candle_handle hdev, uint8_t ch, uint32_t *state, uint32_t *rxerr, uint32_t *txerr); /* Передаём состояние CAN и оба счётчика ошибок без потери данных ответа. */
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame);
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms);
+23 -2
View File
@@ -39,8 +39,10 @@ enum {
CANDLE_TIMESTAMP_GET = 6,
/* 7: IDENTIFY, 8: GET_USER_ID, 9: SET_USER_ID (not used here) */
CANDLE_BREQ_DATA_BITTIMING = 10,
/* 11: SET_TERMINATION, not used here */
CANDLE_BREQ_GET_STATE = 12,
CANDLE_BREQ_BT_CONST_EXT = 11, /* Расширенные пределы номинальной и быстрой фазы согласно gs_usb. */
CANDLE_BREQ_SET_TERMINATION = 12, /* Запрос gs_usb 12 переключает встроенный резистор окончания шины. */
CANDLE_BREQ_GET_TERMINATION = 13, /* Запрос gs_usb 13 читает текущее состояние терминатора. */
CANDLE_BREQ_GET_STATE = 14, /* Стандартный GET_STATE имеет номер 14; номер 12 принадлежит терминатору. */
CANDLE_BREQ_BUS_OFF_RECOVERY = 32,
};
@@ -237,3 +239,22 @@ bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel)
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
return rc;
}
bool candle_ctrl_data_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *cap) /* Структура gs_usb BT_CONST_EXT содержит десять общих и восемь быстрых полей uint32. */
{ /* Используем массив, поскольку протокол содержит только однородные 32-битные значения без отступов. */
uint32_t fields[18] = {0}; /* 18 полей по 4 байта образуют стандартный ответ размером 72 байта. */
bool rc = usb_control_msg(dev->winUSBHandle, CANDLE_BREQ_BT_CONST_EXT, USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE, channel, 0, fields, sizeof(fields)); /* Запрашиваем пределы именно выбранного CAN-канала. */
if (rc) { /* Выходная структура должна меняться только после успешного USB-ответа. */
memcpy(cap, fields, 2 * sizeof(uint32_t)); /* Маска функций и частота CAN общие для обеих фаз. */
memcpy(&cap->tseg1_min, &fields[10], 8 * sizeof(uint32_t)); /* Поля DTSEG1…DBRP_INC заменяют номинальные пределы структуры capability. */
} /* При отказе вызывающий код получит false и не использует нулевые пределы. */
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_BITTIMING_CONST; /* Сохраняем диагностический код чтения аппаратных ограничений. */
return rc; /* Успех означает наличие полной структуры пределов быстрой фазы. */
} /* Завершение запроса расширенных возможностей FD. */
bool candle_ctrl_termination(candle_device_t *dev, uint8_t channel, uint32_t *state, bool write) /* Переключение терминатора использует стандартные SET=12 и GET=13. */
{ /* Состояние передаётся одним 32-битным словом согласно протоколу gs_usb. */
bool rc = usb_control_msg(dev->winUSBHandle, write ? CANDLE_BREQ_SET_TERMINATION : CANDLE_BREQ_GET_TERMINATION, (write ? USB_DIR_OUT : USB_DIR_IN)|USB_TYPE_VENDOR|USB_RECIP_INTERFACE, channel, 0, state, sizeof(*state)); /* Номер CAN-канала передаётся в wValue, а wIndex равен нулю по спецификации. */
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE; /* Ошибка настройки доступна Python через candle_dev_last_error. */
return rc; /* Не выдаём успешное переключение при отказе USB-контроллера. */
} /* Завершение управления встроенным окончанием шины. */
+2
View File
@@ -51,4 +51,6 @@ bool candle_ctrl_set_data_bittiming(candle_device_t *dev, uint8_t channel, candl
bool candle_ctrl_get_timestamp(candle_device_t *dev, uint32_t *current_timestamp);
bool candle_ctrl_get_state(candle_device_t *dev, uint8_t channel, candle_device_state_t *data);
bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel);
bool candle_ctrl_data_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *cap); /* Читаем пределы быстрой фазы CAN FD отдельным запросом расширенной структуры. */
bool candle_ctrl_termination(candle_device_t *dev, uint8_t channel, uint32_t *state, bool write); /* Передаём состояние терминатора: 0 — выключен, 1 — включён. */
+74
View File
@@ -0,0 +1,74 @@
import importlib.util # Загружает общий модуль подготовки непосредственно из проверяемой копии templates.
import json # Проверяет сохранение пользовательских полей при резервировании версии 166.
from pathlib import Path # Все файлы теста находятся в изолированном временном каталоге.
import tempfile # Исключает запись версий и паспортов в рабочие прошивки пользователя.
import unittest # Запускает проверки без зависимостей SETGUI и Qt.
TOOLS = Path(__file__).resolve().parents[1] / "tools" # Тест переносится вместе с c/firmware-info в полный checkout templates.
SPEC = importlib.util.spec_from_file_location("checked_c2000_build", TOOLS / "c2000_build.py") # Выбирает одну реализацию общего процесса.
BUILD = importlib.util.module_from_spec(SPEC) # Импорт не выполняет prebuild и не меняет счётчики версии.
SPEC.loader.exec_module(BUILD) # Проверяет реальный модуль, а не копию функций внутри теста.
class LegacyVersionTests(unittest.TestCase): # Подтверждает совместимость общей подготовки с JSON-конфигурацией 166.
def test_json_counter_increments_once_and_keeps_settings(self): # Перенос в templates не должен удваивать резервирование версии.
with tempfile.TemporaryDirectory() as temporary: # Версия настоящего проекта 166 остаётся неизменной.
project = Path(temporary) # Минимальный проект использует прежние настройки без нового аппаратного профиля.
settings = {"product": "Balsam-166-PM35", "target": "TMS320F28335", "version": [1, 0, 999], "increment_version": True, "max_image_bytes": 32508, "copy_to": "../Bin"} # Patch 999 проверяет перенос в minor по общему контракту версии.
config = project / "firmware_identity.json" # Сохраняемый счётчик расположен в том же JSON, что в ветке 166.
config.write_text(json.dumps(settings), encoding="utf-8") # Русские данные и настройки сохраняются как UTF-8.
templates = TOOLS.parents[2] # Полный путь templates восстанавливается из c/firmware-info/tools.
metadata = BUILD.prepare(project, templates, project / "Debug/generated") # Выполняет настоящий общий prebuild без компиляции и сети.
saved = json.loads(config.read_text(encoding="utf-8")) # Проверяет именно сохранённую версию, а не только возвращённые метаданные.
self.assertEqual(saved["version"], [1, 1, 0]) # Номер увеличен один раз с переносом patch в minor.
self.assertEqual(metadata["version"], saved["version"]) # Паспорт и сохраняемый счётчик описывают одну попытку компиляции.
self.assertEqual(metadata["max_image_bytes"], 32508) # Историческая граница EEPROM 166 остаётся обязательной для postbuild.
self.assertEqual(saved["copy_to"], settings["copy_to"]) # Общий генератор сохраняет пользовательские поля, даже если сеть выполняет Публикатор.
class ReleaseLayoutTests(unittest.TestCase): # Проверяет назначение комплекта F2812 и совместимость экспорта для остальных процессоров.
def test_primary_package_follows_target_and_exports_with_receipt(self): # Проверяет пути, байты и паспорта полного комплекта после раскладки и экспорта.
identity = BUILD.load_identity(TOOLS.parents[2]) # Использует действующие функции подписи и копирования общей библиотеки.
for target, purpose in ((None, "eeprom"), ("TMS320F2812", "flash"), ("TMS320F28335", "eeprom")): # Старые вызовы и F28335 сохраняют EEPROM; F2812 использует внешнюю Flash.
with self.subTest(target=target), tempfile.TemporaryDirectory() as temporary: # Каждый вариант получает независимый выпуск и отдельную диагностику.
root = Path(temporary) # Файлы теста не попадают в действующий проект или каталог загрузок.
stage = root / "stage" # Воспроизводит временный каталог защищённого postbuild.
archive_name = "current-build" # Имя архива здесь фиксировано: проверяется раскладка, а не генерация даты.
archive = stage / archive_name # Полная копия компиляции должна получить те же папки, что текущие результаты.
archive.mkdir(parents=True) # Создаёт только каталоги данного проверочного выпуска.
metadata = {"target": target, "version": [1, 2, 3]} # Проверка паспорта должна сохранить процессор и версию конкретного сценария.
metadata_path = archive / "firmware_identity.json" # Общий паспорт остаётся в корне архива после раскладки.
metadata_path.write_text(json.dumps(metadata), encoding="utf-8") # Записывает метаданные в штатной кодировке общей библиотеки.
artifact, image_name = "Firmware", "Firmware_NOGIT000.bin" # Различает исходный OUT и BIN с идентификатором сборки.
companions = {"Firmware.out": b"original OUT", "Firmware.hex": b"original HEX", image_name + ".map": b"original map"} # Сопроводительные файлы обязаны следовать за основным образом без изменения байтов.
variants = {"Firmware_flash.out": "flash", "Firmware_flash_rs.bin": "rs", "Firmware_NOGIT000_can.bin": "can"} # Именованные дополнительные варианты сохраняют прежние независимые назначения.
for folder in (stage, archive): # Готовит одинаковые результаты для текущего выпуска и полного архива.
image = folder / image_name # Каждый экземпляр BIN получает собственные настоящие manifest и SHA-256.
image.write_bytes(b"primary SCI8 payload") # Узнаваемые байты выявляют подмену образа при переносе или экспорте.
identity.seal(image, metadata_path) # Вычисляет контрольные суммы штатной реализацией, а не подставляет фиктивный manifest.
for name, content in companions.items(): # Подготавливает OUT, HEX и карту того же основного комплекта.
(folder / name).write_bytes(content) # Сохраняет эталон для проверки точности переноса.
for name in variants: # Дополнительные образы нужны для проверки сохранения маршрутов flash, rs и can.
(folder / name).write_bytes(name.encode("ascii")) # Отличимые имена в содержимом исключают случайную перестановку файлов.
relative = BUILD.arrange_release(stage, archive_name, artifact, image_name, Path("Firmware_NOGIT000_can.bin"), target=target) # Вызывает ту же раскладку, что действующий postbuild.
self.assertEqual(relative, purpose + "/" + image_name) # Возвращаемый путь должен вести к основному образу в назначении выбранного процессора.
BUILD.write_receipt(stage, archive_name, relative) # Проверяет связь раскладки с указателем последнего выпуска.
receipt = json.loads((stage / "latest_build.json").read_text(encoding="utf-8")) # Читает записанный указатель как его потребитель.
self.assertEqual(receipt["manifest"], relative + ".manifest.json") # Путь импорта должен учитывать flash у F2812 и eeprom у остальных.
exported = identity.export_build(archive, root / "export", relative) # Выполняет настоящий экспорт всего архива в отдельный каталог.
for folder in (stage, archive, exported): # Одновременно проверяет локальный результат, архив и копию для пользователя.
BUILD.verify_package(folder / relative, metadata) # Сверяет реальные байты BIN, размер, SHA-256 и поля паспорта после всех переносов.
self.assertEqual((folder / relative).read_bytes(), b"primary SCI8 payload") # Основной образ сохраняется побайтово.
for suffix in (".manifest.json", ".sha256"): # Оба сопроводительных файла должны лежать рядом с BIN.
self.assertTrue((folder / (relative + suffix)).is_file()) # Обнаруживает потерю manifest или контрольной суммы при раскладке.
for name, content in companions.items(): # OUT, HEX и карта должны повторять назначение основного BIN.
self.assertEqual((folder / purpose / name).read_bytes(), content) # Проверяет путь и точное содержимое каждого файла.
for name, destination in variants.items(): # Проверяет неизменность маршрутов дополнительных вариантов.
self.assertEqual((folder / destination / name).read_bytes(), name.encode("ascii")) # Отдельные flash, rs и can не должны уйти в папку основного образа.
self.assertTrue((folder / "firmware_identity.json").is_file()) # Общий паспорт остаётся доступен в корне каждого комплекта.
if target == "TMS320F2812": # Только для F2812 проверяет отсутствие вводящей в заблуждение папки EEPROM.
self.assertFalse((folder / "eeprom").exists()) # Новый выпуск F2812 должен содержать основной комплект исключительно в flash.
if __name__ == "__main__": # Импорт набора проверок не запускает их повторно.
unittest.main() # Позволяет проверить совместимость прямой командой Python.
@@ -0,0 +1,87 @@
"""Проверяет совместимость postbuild, экспорт двух образов и отказ от устаревшего Flash OUT.""" # Фиксирует контракт общего выпуска без зависимости от установленного TI-компилятора.
import json # Формирует паспорт текущей сборки для проверки архива и контрольных сумм.
from pathlib import Path # Создаёт независимые пути проекта, компилятора и результатов в каждом тесте.
import subprocess # Использует штатное исключение ненулевого кода проектного сборщика.
import sys # Подключает общий модуль из каталога tools текущей копии templates.
import tempfile # Изолирует проверочные файлы от реальных прошивок и каталогов GUI.
import unittest # Организует проверки публичного контракта postbuild стандартной библиотекой Python.
from unittest.mock import patch # Подменяет только внешние инструменты, оставляя настоящее архивирование и SHA-256.
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'tools')) # Находит проверяемую реализацию относительно теста без путей конкретной рабочей станции.
import c2000_postbuild as c2000 # Вызывает тот же postbuild, который запускается штатным postcompile.bat.
from identity import verify # Проверяет байты скопированных образов по настоящему manifest.
class PostbuildTests(unittest.TestCase): # Проверяет общую цепочку от готового OUT до экспортированной папки выпуска.
def setUp(self): # Для каждой проверки создаёт отдельный проект с типичной структурой CCS.
temporary = tempfile.TemporaryDirectory() # Все файлы сценария принадлежат временному каталогу теста.
self.addCleanup(temporary.cleanup) # Освобождает только каталог данной проверки при любом результате.
self.root = Path(temporary.name) # Корень не зависит от реальной прошивки и установленного templates.
self.build = self.root / 'project/Debug' # Проверяет соглашение о расположении сборщика рядом с конфигурациями CCS.
self.build.mkdir(parents=True) # Подготавливает рабочий каталог внешних инструментов.
self.compiler = self.root / 'compiler' # Позволяет проверить передачу компилятора текущего проекта вместо фиксированного пути.
(self.compiler / 'bin').mkdir(parents=True) # Воспроизводит структуру установленного C2000 compiler.
(self.compiler / 'bin/hex2000.exe').write_bytes(b'fake tool') # Проверка наличия инструмента реальна, а выполнение будет подменено.
self.source = self.root / 'Bin/Firmware.out' # Основной OUT размещён вне Debug, как в рабочем проекте пользователя.
self.source.parent.mkdir() # Создаёт отдельный каталог исходного COFF и итогового Flash COFF.
self.source.write_bytes(b'RAM image') # Даёт узнаваемые байты для проверки, что основной образ не подменён Flash-вариантом.
self.metadata = self.build / 'metadata.json' # Один паспорт используется обоими вариантами текущего выпуска.
self.metadata.write_text(json.dumps({'build_id': 'NOGIT000', 'built_at_utc': '2026-10-14T13:00:00+00:00', 'source': {'commit': None}, 'version': [1, 2, 3]}), encoding='utf-8') # Фиксированное время проверяет выбор папки по prebuild, а не времени файлов.
self.output = self.build / 'firmware' # Локальный выпуск получает оба образа и их сопроводительные файлы.
self.export = self.root / 'GUI' # Экспорт изолирован от настоящего GUI и сетевых ресурсов.
def fake_tool(self, command, **kwargs): # Имитирует границу внешних инструментов, оставляя реальную логику упаковки и проверки.
if '--postbuild' in command: # Вложенная Flash-сборка обязана использовать режим без повторного Make.
self.assertEqual(Path(command[command.index('--compiler') + 1]), self.compiler.resolve()) # Проверяет выбор текущей версии TI compiler.
self.assertEqual(Path(command[command.index('--build-dir') + 1]), self.build.resolve()) # Проверяет использование текущих объектов и generated-паспорта.
image = Path(command[command.index('--output') + 1]) # Результат должен создаваться по переданному временному пути.
self.assertNotEqual(image.parent, self.source.parent) # Запрещает выдавать существующий OUT из Bin за результат текущего вызова.
image.write_bytes(b'current Flash image') # Создаёт узнаваемый новый COFF для сравнения всех копий.
image.with_suffix('.map').write_text('codestart 0033fff6', encoding='utf-8') # Воспроизводит обязательный второй результат сборщика.
else: # Обычная SCI8-конверсия должна продолжать использовать основной RAM OUT.
expected = 'Firmware_flash.out' if '_flash_rs.bin' in command[command.index('-o') + 1] else self.source.name # Только отдельная RS-конверсия должна брать Flash COFF.
self.assertEqual(Path(command[1]).name, expected) # Проверяет, что EEPROM BIN и Flash RS BIN создаются из разных исходных образов.
Path(command[command.index('-o') + 1]).write_bytes(b'EEPROM image') # Выпускает HEX/BIN с отличимыми байтами основного варианта.
Path(command[command.index('-map') + 1]).write_text('RAM map', encoding='utf-8') # Сохраняет карты, необходимые настоящему архиватору.
return subprocess.CompletedProcess(command, 0) # Подтверждает только имитируемый инструмент, не подменяя проверки выпуска.
def run_postbuild(self): # Вызывает публичную функцию с теми же путями, что передаёт BAT из CCS.
return c2000.postbuild(self.compiler, self.build, str(self.source.with_suffix('')), self.metadata, self.output, self.export) # Включает настоящий экспорт и проверку manifest.
def test_project_without_flash_builder_keeps_eeprom_workflow(self): # Старые проекты должны работать без новых настроек или Flash-файлов.
with patch.object(c2000.subprocess, 'run', side_effect=self.fake_tool) as calls: # Подменяет только TI Hex Utility.
result = self.run_postbuild() # Выполняет полный прежний выпуск EEPROM.
self.assertEqual(calls.call_count, 2) # Для HEX и BIN выполнены два вызова; дополнительного сборщика не было.
self.assertEqual(self.source.read_bytes(), b'RAM image') # Основной OUT не изменён упаковкой.
self.assertFalse(self.source.with_name('Firmware_flash.out').exists()) # Отсутствие builder не создаёт фиктивный Flash-образ.
self.assertEqual(verify(result, Path(str(result) + '.manifest.json'))['size_bytes'], len(b'EEPROM image')) # Проверяет настоящее содержимое EEPROM BIN и его паспорт.
def test_flash_and_eeprom_share_current_export_with_separate_checksums(self): # Новый выпуск должен содержать оба варианта под своими именами.
(self.build.parent / 'build_flash.py').write_text('# Сборщик проекта подключён явно.\n', encoding='utf-8') # Наличие сценария включает дополнительный шаг postbuild.
with patch.object(c2000.subprocess, 'run', side_effect=self.fake_tool) as calls: # Flash и hex2000 имитируются на границе процессов.
self.run_postbuild() # Архиватор и экспорт работают без подмены.
self.assertEqual(calls.call_count, 4) # Один Flash-вызов, две EEPROM-конверсии и отдельный RS BIN подтверждают отсутствие рекурсивного Make.
folder = self.export / 'NOGIT000_2026-10-14_16-00-00_UTC+3' # Имя следует времени prebuild с переводом в московский UTC+3.
for directory in (self.source.parent, self.output, folder): # Проверяет копию для CCS, плоский выпуск и архив для GUI.
self.assertEqual((directory / 'Firmware_flash.out').read_bytes(), b'current Flash image') # Каждая копия содержит новый Flash COFF.
self.assertTrue((directory / 'Firmware_flash.map').is_file()) # Карта Flash не теряется при сохранении или экспорте.
for name in ('Firmware_flash.out', 'Firmware_NOGIT000.bin', 'Firmware_flash_rs.bin'): # Контрольные суммы каждого варианта должны проверяться независимо.
verify(folder / name, folder / (name + '.manifest.json')) # Использует реальные SHA-256 экспортированных байтов.
self.assertEqual((folder / self.source.name).read_bytes(), b'RAM image') # Архив сохраняет исходный RAM OUT вместе с новым Flash OUT.
def test_failed_or_incomplete_builder_cannot_export_stale_flash(self): # Ошибка или отсутствие карты должны остановить выпуск до экспорта старого COFF.
(self.build.parent / 'build_flash.py').write_text('# Подключён неисправный сборщик для проверки отказа.\n', encoding='utf-8') # Включает путь Flash-проверки.
stale = self.source.with_name('Firmware_flash.out') # В Bin уже есть образ от предыдущей успешной сборки.
stale.write_bytes(b'old Flash image') # Отличимые байты выявят ошибочную повторную публикацию.
for failure in (subprocess.CalledProcessError(1, 'builder'), None): # Проверяет ненулевой код и ложный успех без обязательных результатов.
with self.subTest(failure=failure): # Даёт отдельную диагностику двум независимым отказам.
with patch.object(c2000.subprocess, 'run', side_effect=failure) as calls: # Сборщик либо падает, либо не создаёт файлов.
with self.assertRaises((subprocess.CalledProcessError, ValueError)): # Оба отказа должны выйти наружу как ошибка postbuild.
self.run_postbuild() # Не разрешает продолжение EEPROM-конверсии после сбоя Flash.
self.assertEqual(calls.call_count, 1) # После отказа Flash ни hex2000, ни новый сборщик не запускаются.
self.assertEqual(stale.read_bytes(), b'old Flash image') # Предыдущий образ сохраняется, но не считается новым результатом.
self.assertFalse(self.export.exists()) # Неполная сборка не создаёт выпуск для GUI.
if __name__ == '__main__': # Позволяет запускать проверки отдельно или через unittest discover.
unittest.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() # Позволяет выполнить проверку отдельно от полного набора тестов библиотеки.
+16
View File
@@ -39,6 +39,22 @@ class IdentityTests(unittest.TestCase):
self.assertEqual(self.prepare([clean,dirty])["build_id"], "abcdef0+")
self.assertEqual(self.prepare([dirty,clean])["build_id"], "abcdef0+")
def test_project_and_repository_survive_sealing(self): # Проверяет сведения, нужные Публикатору после переноса сборки на другой компьютер.
for project_name in (None, "Бальзам — ПМ35"): # Поддерживает прежние конфигурации и явно заданное русское название.
settings = {"product": "PM35", "target": "TMS320F28335", "version": [1, 2, 3]} # Код изделия сохраняет техническое назначение независимо от названия проекта.
if project_name is not None: # Отсутствующее поле должно использовать совместимое значение по умолчанию.
settings["project"] = project_name # Название из конфигурации переносится в паспорт без сокращений.
self.config.write_text(json.dumps(settings), encoding="utf-8") # Конфигурация сохраняется в той же кодировке, что и проект пользователя.
clean = {"commit": "a" * 40, "dirty": False} # Источник и templates остаются известными чистыми состояниями.
metadata = self.prepare([dict(clean), dict(clean)]) # Генерирует настоящий precompile JSON и заголовок без запуска компилятора.
self.assertEqual(metadata["project"], project_name or "PM35") # Старый JSON получает код изделия, новый — заданное название.
self.assertEqual(metadata["source"]["repository"], str(self.root.resolve())) # Абсолютный путь сохраняется в состоянии исходного проекта.
image = self.root / "controller.bin" # Отдельный образ позволяет проверить последующий postbuild manifest.
image.write_bytes(b"firmware fixture") # Небольшой файл достаточен для проверки сохранения полей и SHA-256.
sealed = identity.seal(image, self.root / "firmware_identity.json") # Штатный postbuild дополняет сохранённое происхождение без чтения нового HEAD.
self.assertEqual(sealed["project"], metadata["project"]) # Название доходит до того manifest, который импортирует GUI.
self.assertEqual(sealed["source"], metadata["source"]) # Путь, полный коммит и признак dirty переживают запечатывание образа.
def test_missing_git_is_explicit(self):
unknown = {"commit":None,"dirty":None}
self.assertEqual(self.prepare([unknown,unknown])["build_id"], "NOGIT000")
@@ -0,0 +1,111 @@
"""Tests for preserving exact firmware bytes under pre-build provenance folders."""
import json
from pathlib import Path
import sys
import tempfile
import unittest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools"))
from identity import archive_build, build_folder_name, export_build, verify
class ArchiveTests(unittest.TestCase):
def metadata(self, stamp="2026-10-08T12:30:00+00:00"):
return {"source": {"commit": "a1b2c3d4" + "0" * 32, "dirty": False},
"templates": {"commit": None, "dirty": None},
"build_id": "NOGIT000", "built_at_utc": stamp,
"version": [1, 2, 3]}
def test_snapshot_preserves_image_and_records_exact_sha(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image = root / "firmware.bin"
payload = bytes(range(256))
image.write_bytes(payload)
metadata = root / "metadata.json"
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
result = archive_build(image, metadata, root / "out")
folder = Path(result["directory"])
self.assertEqual(folder.name, "a1b2c3d4_2026-10-08_15-30-00_UTC+3")
self.assertEqual((folder / image.name).read_bytes(), payload)
self.assertEqual(image.read_bytes(), payload)
self.assertEqual(json.loads((folder / "firmware_identity.json").read_text()), self.metadata())
manifest = folder / (image.name + ".manifest.json")
self.assertEqual(verify(folder / image.name, manifest)["size_bytes"], 256)
(folder / image.name).write_bytes(b"corrupted")
with self.assertRaises(ValueError):
verify(folder / image.name, manifest)
def test_prebuild_times_keep_two_builds_separate(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.hex", root / "metadata.json"
image.write_bytes(b":00000001FF\r\n")
folders = []
for stamp in ("2026-10-08T12:30:00+00:00", "2026-10-08T12:31:00+00:00"):
metadata.write_text(json.dumps(self.metadata(stamp)), encoding="utf-8")
folders.append(Path(archive_build(image, metadata, root / "out")["directory"]))
self.assertNotEqual(folders[0], folders[1])
self.assertTrue(all((folder / image.name).is_file() for folder in folders))
def test_export_copies_current_folder_only(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.bin", root / "metadata.json"
image.write_bytes(b"exact current build")
# A folder with a later timestamp must not be selected for this build.
metadata.write_text(json.dumps(self.metadata("2026-10-08T12:40:00+00:00")), encoding="utf-8")
archive_build(image, metadata, root / "local")
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
result = archive_build(image, metadata, root / "local", export=root / "exported")
copied = Path(result["exported_directory"])
self.assertEqual([p.name for p in (root / "exported").iterdir()],
["a1b2c3d4_2026-10-08_15-30-00_UTC+3"])
self.assertEqual((copied / image.name).read_bytes(), image.read_bytes())
self.assertTrue((copied / "firmware_identity.json").is_file())
self.assertTrue((copied / (image.name + ".sha256")).is_file())
verify(copied / image.name, copied / (image.name + ".manifest.json"))
def test_complete_folder_export_keeps_nested_files_and_old_destination(self):
with tempfile.TemporaryDirectory() as root:
root = Path(root)
image, metadata = root / "firmware.bin", root / "metadata.json"
image.write_bytes(b"image")
metadata.write_text(json.dumps(self.metadata()), encoding="utf-8")
folder = Path(archive_build(image, metadata, root / "local")["directory"])
(folder / "notes").mkdir()
(folder / "notes" / "release.txt").write_text("release notes", encoding="utf-8")
old = root / "exported" / "old_build"
old.mkdir(parents=True)
(old / "keep.txt").write_text("keep", encoding="utf-8")
copied = export_build(folder, root / "exported", image.name)
self.assertEqual((copied / "notes" / "release.txt").read_text(), "release notes")
self.assertEqual((old / "keep.txt").read_text(), "keep")
with self.assertRaises(ValueError):
export_build(folder, folder / "recursive", image.name)
def test_offset_normalizes_to_utc_plus_three_and_dirty_is_visible(self):
metadata = self.metadata("2026-10-08T15:30:00+03:00")
metadata["source"]["dirty"] = True
self.assertEqual(build_folder_name(metadata), "a1b2c3d4+_2026-10-08_15-30-00_UTC+3")
def test_folder_date_rolls_over_at_utc_plus_three_midnight(self):
metadata = self.metadata("2026-10-08T22:30:00+00:00")
self.assertEqual(build_folder_name(metadata), "a1b2c3d4_2026-10-09_01-30-00_UTC+3")
self.assertEqual(metadata["built_at_utc"], "2026-10-08T22:30:00+00:00")
def test_archive_without_git_has_explicit_placeholder(self):
metadata = self.metadata()
metadata["source"]["commit"] = None
self.assertEqual(build_folder_name(metadata), "NOGIT000_2026-10-08_15-30-00_UTC+3")
def test_invalid_provenance_cannot_be_a_folder_name(self):
metadata = self.metadata()
metadata["source"]["commit"] = "../outside"
with self.assertRaises(ValueError):
build_folder_name(metadata)
metadata = self.metadata("2026-10-08T12:30:00")
with self.assertRaises(ValueError):
build_folder_name(metadata)
if __name__ == "__main__":
unittest.main()
@@ -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.
@@ -0,0 +1,83 @@
"""Проверяет общий сохраняемый итератор версии до компиляции CCS, Keil и CLI.""" # Запуск: python -m unittest discover -s c/firmware-info/tests -p test_version_iterator.py.
import importlib.util # Загружает тот же генератор, который вызывается штатными BAT templates.
import json # Проверяет исходный JSON и метаданные после резервирования версии.
from pathlib import Path # Изолирует проверочные настройки и generated во временном каталоге.
import tempfile # Удаляет только собственные файлы после каждой проверки.
import unittest # Проверяет последовательность номеров, ручной режим и отказы записи.
from unittest.mock import patch # Воспроизводит ошибки файловой системы без изменения прав пользователя.
TOOL = Path(__file__).resolve().parents[1] / "tools/identity.py" # Находит общую реализацию относительно файла теста.
spec = importlib.util.spec_from_file_location("shared_iterator", TOOL) # Не импортирует проектные обёртки или другой генератор из PATH.
identity = importlib.util.module_from_spec(spec) # Создаёт отдельный экземпляр модуля для изолированной подмены отказов.
spec.loader.exec_module(identity) # Загружает функции без выполнения CLI и без изменения рабочих версий.
class VersionIteratorTests(unittest.TestCase): # Проверяет поведение общего prepare независимо от IDE.
def setUp(self): # Готовит настройки другого изделия только во временном каталоге.
temporary = tempfile.TemporaryDirectory(prefix="shared iterator ") # Пробелы в пути не должны влиять на атомарную запись настроек.
self.addCleanup(temporary.cleanup) # Удаляет временные файлы даже после ожидаемого отказа генерации.
self.root = Path(temporary.name) # Ограничивает все изменения проверки собственным каталогом.
self.config = self.root / "config.json" # Отделяет исходную настройку от generated/firmware_identity.json.
self.output = self.root / "generated" # Моделирует отдельный каталог текущей конфигурации сборки.
self.settings = {"product": "Другое изделие", "target": "host", "version": [1, 0, 4], "increment_version": True, "extra": {"keep": "настройка"}} # Дополнительное поле проверяет сохранность проектных настроек в UTF-8.
self.save() # Устанавливает номер, на котором обнаружилась остановка прямого подключения.
def save(self): # Сохраняет выбранные входные настройки для текущего сценария.
self.config.write_text(json.dumps(self.settings, ensure_ascii=False), encoding="utf-8") # Не использует рабочий счётчик какого-либо проекта.
def prepare(self): # Запускает публичный общий API с детерминированным состоянием Git.
with patch.object(identity, "git_state", return_value={"commit": None, "dirty": None}): # Доступ к Git не влияет на проверку счётчика.
return identity.prepare(self.root, self.root, self.config, self.output) # Вызывает тот же prepare, что CCS, Keil и CLI.
def test_sequence_and_matching_outputs(self): # Воспроизводит увеличение после 1.0.4 и отсутствие повторного номера.
for expected in ([1, 0, 5], [1, 0, 6]): # Каждый общий pre-build должен занимать ровно один номер.
metadata = self.prepare() # Генерирует реальный заголовок и JSON после атомарного резервирования.
settings = json.loads(self.config.read_text(encoding="utf-8")) # Читает сохранённый источник следующей версии.
self.assertEqual(settings["version"], expected) # Проверяет увеличение исходного JSON, а не только отображаемой версии.
self.assertEqual(settings["extra"], self.settings["extra"]) # Сохраняет неизвестные общему генератору поля проекта.
self.assertEqual(metadata["version"], expected) # Возвращаемые метаданные содержат уже занятый номер.
self.assertEqual(json.loads((self.output / "firmware_identity.json").read_text(encoding="utf-8"))["version"], expected) # Сохранённый паспорт совпадает с источником версии.
self.assertIn(f"#define FIRMWARE_VERSION_PATCH {expected[2]}U", (self.output / "firmware_identity.h").read_text(encoding="utf-8")) # Компилятор получает тот же patch, что manifest.
def test_manual_mode_preserves_config_bytes(self): # Старые проекты без флага и явно отключённый итератор сохраняют ручную версию.
for enabled in (None, False): # None обозначает отсутствие поля, а не допустимое значение JSON-флага.
self.settings.pop("increment_version", None) # Сначала моделирует старую конфигурацию без нового переключателя.
if enabled is False: self.settings["increment_version"] = False # Затем проверяет явное отключение без удаления поля.
self.save() # Записывает исходный формат, который генератор не должен переформатировать.
original = self.config.read_bytes() # Сравнивает байты, чтобы заметить любую лишнюю запись.
self.assertEqual(self.prepare()["version"], [1, 0, 4]) # Ручной номер используется без увеличения.
self.assertEqual(self.config.read_bytes(), original) # Исходный JSON остаётся побайтно прежним.
def test_carry_and_exhaustion(self): # Проверяет ограничения трёх разрядов контракта firmware-info v1.
for previous, expected in (([1, 0, 999], [1, 1, 0]), ([1, 255, 999], [2, 0, 0])): # Patch переносится после 999, minor после 255.
self.settings["version"] = previous # Задаёт последнюю версию перед границей разряда.
self.save() # Передаёт граничное значение публичному prepare через JSON.
self.assertEqual(self.prepare()["version"], expected) # Проверяет перенос вместе с реальной записью настроек.
self.settings["version"] = [255, 255, 999] # Устанавливает последний допустимый номер всего контракта.
self.save() # Сохраняет исходный предел перед ожидаемым отказом.
original = self.config.read_bytes() # Переполнение не должно сбрасывать номер в JSON.
with self.assertRaises(ValueError): self.prepare() # Требует явную ошибку вместо повторного использования версии.
self.assertEqual(self.config.read_bytes(), original) # Сохраняет последний занятый номер без частичной записи.
def test_invalid_flag_does_not_change_config(self): # Не допускает неявное включение счётчика строками, числами или null.
for value in ("true", "false", 1, 0, None, [], {}): # Только JSON true и false являются допустимыми значениями переключателя.
self.settings["increment_version"] = value # Передаёт неверный тип через настоящий JSON.
self.save() # Фиксирует исходные байты для проверки отказа до резервирования.
original = self.config.read_bytes() # Ошибка настройки не должна занимать номер версии.
with self.assertRaises(ValueError): self.prepare() # Останавливает генерацию до изменения счётчика.
self.assertEqual(self.config.read_bytes(), original) # Подтверждает сохранение исходного JSON при отказе проверки.
def test_generation_failure_consumes_number(self): # Номер неудачной попытки не должен использоваться следующей сборкой.
with patch.object(identity, "write_atomic", side_effect=OSError("header unavailable")): # Воспроизводит отказ записи заголовка после сохранения следующей версии.
with self.assertRaises(OSError): self.prepare() # Передаёт ошибку вызывающему BAT без маскировки успешным завершением.
self.assertEqual(json.loads(self.config.read_text(encoding="utf-8"))["version"], [1, 0, 5]) # Резервирование фиксируется до генерации выходных файлов.
self.assertEqual(self.prepare()["version"], [1, 0, 6]) # Следующая попытка занимает следующий номер, даже если прежняя не дошла до компиляции.
def test_failed_config_replace_preserves_original(self): # Атомарная замена не должна портить настройки при отказе файловой системы.
original = self.config.read_bytes() # Сохраняет эталон исходного номера и остальных полей.
with patch.object(identity.Path, "replace", side_effect=PermissionError("config unavailable")): # Имитирует отказ фиксации временного файла вместо записи нового JSON.
with self.assertRaises(PermissionError): self.prepare() # Останавливает pre-build, если номер нельзя надёжно сохранить.
self.assertEqual(self.config.read_bytes(), original) # Исходные настройки не заменены частичным или неверным файлом.
self.assertEqual(list(self.root.glob(".firmware-version-*")), []) # При отказе не остаются собственные временные файлы счётчика.
if __name__ == "__main__": unittest.main() # Любое нарушение общего итератора возвращает ненулевой код процесса.
+449
View File
@@ -0,0 +1,449 @@
"""Единая подготовка паспорта и проверенного RAM/Flash-комплекта C2000 для CCS и Публикатора.""" # Сетевой выпуск выполняет отдельный Публикатор SETGUI после импорта подготовленной папки.
import argparse # Разбирает одинаковые аргументы защищённого postbuild для CCS Debug и Release.
import base64 # Декодирует точные байты секций COFF из официального XML-вывода TI ofd2000.
from datetime import datetime # Восстанавливает слова UTC из паспорта, созданного перед компиляцией.
import hashlib # Проверяет точные байты конечного BIN и сопроводительного SHA-256.
import importlib.util # Подключает штатный механизм экспорта только после проверки временного выпуска.
import json # Сверяет изделие, аппаратный профиль и идентификацию текущей сборки.
from pathlib import Path # Разрешает пути относительно выбранной конфигурации CCS, а не текущего терминала.
import re # Читает абсолютные EEPROM-символы из карты линкера TI.
import shutil # Копирует только полностью проверенные файлы из временного каталога.
import struct # Разбирает 16-битные слова SCI8 и адреса загрузочных блоков C28x.
import subprocess # Вызывает общую BAT-точку конверсии без публикации во внешнее приложение.
import tempfile # Изолирует непроверенные результаты общего postcompile от постоянных выпусков.
import xml.etree.ElementTree as ET # Проверяет фактические LOAD/RUN адреса Flash COFF без собственного двоичного парсера TI.
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 для пользовательских заголовков конфигурации.
text = re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S) # Удаляет комментарии перед поиском настоящих директив препроцессора.
values = re.findall(r"^\s*#\s*define\s+" + re.escape(name) + r"\s+([^\n]+)", text, re.M) # Требует определение нужного макроса, а не упоминание в тексте.
if len(values) != 1 or not re.fullmatch(r"(?:0[xX][0-9a-fA-F]+|[0-9]+)[uUlL]*", values[0].strip()): # Исключает неоднозначность и выражения, которые могут отличаться от C-препроцессора.
raise ValueError(f"{header}: define {name} once as an integer") # Останавливает выпуск при неподдерживаемой структуре общей разметки.
return int(values[0].strip().rstrip("uUlL"), 0) # Преобразует десятичную либо шестнадцатеричную величину в число Python.
def git_state(repository): # Снимает происхождение выбранного рабочего дерева с локальным safe.directory без изменения глобальных настроек Git.
directory = repository.resolve() # Рабочий каталог CCS может быть вложен в репозиторий приложения.
trusted = next((path for path in (directory, *directory.parents) if (path / ".git").exists()), directory) # Git проверяет корень checkout; .git-файл также учитывает подключённый templates submodule.
command = ["git", "-c", "safe.directory=" + trusted.as_posix(), "-C", str(directory)] # Локальное доверие ограничено корнем явно выбранной зависимости или исходников проекта.
try: # Отсутствие Git сохраняет прежний явный NOGIT-контракт общей библиотеки.
commit = subprocess.check_output(command + ["rev-parse", "HEAD"], stderr=subprocess.DEVNULL, text=True, encoding="utf-8").strip() # Получает полный SHA именно компилируемого дерева.
dirty = bool(subprocess.check_output(command + ["status", "--porcelain", "--untracked-files=normal"], stderr=subprocess.DEVNULL, text=True, encoding="utf-8").strip()) # Учитывает новые и изменённые исходники при формировании знака плюс.
return {"commit": commit, "dirty": dirty} # Возвращает совместимую с общим генератором структуру происхождения.
except (OSError, subprocess.CalledProcessError): # Не выдаёт неизвестную ревизию за чистое дерево.
return {"commit": None, "dirty": None} # Генератор пометит такую сборку идентификатором NOGIT000.
# Объявляет функцию read_version и её входные параметры.
def read_version(header):
# Читает исходный файл или образ для последующей проверки и разбора.
text = header.read_text(encoding="utf-8-sig")
# Задаёт text по выражению справа; значение используется последующими операциями.
text = re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S)
# Задаёт version по выражению справа; значение используется последующими операциями.
version = []
# Перебирает part, limit in (("MAJOR", 255), ("MINOR", 255), ("PATCH", 999)) в указанном порядке.
for part, limit in (("MAJOR", 255), ("MINOR", 255), ("PATCH", 999)):
# Задаёт name по выражению справа; значение используется последующими операциями.
name = "FIRMWARE_VERSION_" + part
# Задаёт definitions по выражению справа; значение используется последующими операциями.
definitions = re.findall(r"^\s*#\s*define\s+" + name + r"\b([^\n]*)", text, re.M)
# Проверяет условие if len(definitions) != 1 перед выполнением следующего блока.
if len(definitions) != 1:
# Останавливает текущую операцию с исключением ValueError(f"{header}: define {name} exactly once").
raise ValueError(f"{header}: define {name} exactly once")
# Задаёт value по выражению справа; значение используется последующими операциями.
value = definitions[0].strip()
# Проверяет условие if not re.fullmatch(r"(?:0|[1-9][0-9]*)[uU]?", value) перед выполнением следующего блока.
if not re.fullmatch(r"(?:0|[1-9][0-9]*)[uU]?", value):
# Останавливает текущую операцию с исключением ValueError(f"{header}: {name} must be a decimal integer").
raise ValueError(f"{header}: {name} must be a decimal integer")
# Задаёт number по выражению справа; значение используется последующими операциями.
number = int(value.rstrip("uU"))
# Проверяет условие if not 0 <= number <= limit перед выполнением следующего блока.
if not 0 <= number <= limit:
# Останавливает текущую операцию с исключением ValueError(f"{header}: {name} must be in 0..{limit}").
raise ValueError(f"{header}: {name} must be in 0..{limit}")
# Продолжает текущий вызов, список или выражение: version.append(number); порядок аргументов сохраняется.
version.append(number)
# Возвращает version вызывающему коду и завершает функцию.
return version
def read_policy(project): # Загружает декларативные настройки памяти платы, оставляя общий алгоритм в templates.
path = project / "firmware_build_profile.json" # Профиль расположен рядом с CCS-проектом и версионируется вместе с linker-файлами.
return json.loads(path.read_text(encoding="utf-8-sig")) if path.is_file() else {} # Старые проекты 166 используют прежний JSON без обязательного нового файла.
def configured_version(project, settings): # Поддерживает ручной C-заголовок 167 и сохраняемый JSON-счётчик 166 одной реализацией.
header = read_policy(project).get("version_header") # Источник версии задаётся платой явно, без определения модели по имени каталога.
return read_version(project / header) if header else settings["version"] # Проверку диапазонов JSON выполняет существующий генератор identity.
def read_profile(project, templates, eeprom=None): # Получает физическую EEPROM из тех же C-заголовков, которыми пользуется компилятор.
policy = read_policy(project) # Все аппаратные пути и имена макросов остаются данными конкретной платы.
can_profile = policy.get("can_boot_profile") # Явное включение CAN-выпуска не меняет поведение старых проектов без этого поля.
if can_profile not in (None, "f28335-m95m02-ab"): # Поддерживаемый профиль соответствует реальному порту M95M02 с двумя слотами по 130560 октетов.
raise ValueError("Unsupported CAN boot profile: " + str(can_profile)) # Опечатка в профиле не должна незаметно отключать обязательный CAN-образ.
config = policy.get("eeprom") # Отсутствие профиля сохраняет контракт 166 с max_image_bytes в основном JSON.
if not config: # У старого проекта нет дополнительной аппаратной разметки для проверки MAP.
path = project / "firmware_identity.json" # Прямой общий postcompile также поддерживает отдельно переданный готовый паспорт.
settings = json.loads(path.read_text(encoding="utf-8-sig")) if path.is_file() else {} # Отсутствие проектной конфигурации не создаёт вымышленные ограничения памяти.
limit = settings.get("max_image_bytes") # Прежняя конфигурация 166 задаёт предел в физических октетах полного SCI8.
if limit is not None and (type(limit) is not int or limit < 24): # Не допускает отрицательную ёмкость, bool или размер меньше заголовка и терминатора SCI8.
raise ValueError("max_image_bytes must be an integer >= 24") # Не создаёт паспорт с непригодным проектным ограничением.
return {"max_image_bytes": limit} if limit is not None else {} # Старый предел EEPROM также применяется общим postbuild.
selected = numeric_define(project / config["selection_header"], config["selection_macro"]) # Читает только числовое определение, исключая исполнение текста заголовка.
variants = config["variants"] # Таблица связывает значение compiler define, название микросхемы и макрос физической ёмкости.
if str(selected) not in variants: # Неизвестное значение конфигурации не подменяется произвольным профилем.
raise ValueError("Unsupported EEPROM selection: " + str(selected)) # Ошибка возникает до компиляции и создания постоянного выпуска.
chosen = next((value for value in variants.values() if value["name"] == eeprom), None) if eeprom else variants[str(selected)] # CLI override сохраняет согласованный выбор отдельной тестовой сборки.
if chosen is None: # Неподдерживаемый профиль запрещён и при программном вызове общего модуля.
raise ValueError("Unsupported EEPROM profile: " + str(eeprom)) # Не создаёт паспорт с вымышленной ёмкостью.
layout = templates / config["layout_header"] # Разметка читается из фактически компилируемой зависимости, а не из другой копии templates.
total = numeric_define(layout, chosen["bytes_macro"]) # Ёмкость выражена физическими октетами EEPROM согласно общему C-контракту.
words = numeric_define(layout, config["param_words_macro"]) # Число параметров выражено 16-битными словами C28x и включает контрольную сумму.
limit = total - (words + 1) * 2 # Сохраняет параметры и последнее резервное слово; одно слово EEPROM занимает два октета.
if total <= 0 or total % 2 or words <= 0 or limit < 24: # Полная SCI8-таблица требует минимум 22 октета заголовка и два октета терминатора.
raise ValueError("Invalid EEPROM layout") # Не выпускает образ с отрицательной или неполной областью прошивки.
result = {"eeprom": chosen["name"], "eeprom_bytes": total, "eeprom_param_words": words, "max_image_bytes": limit, "ram_ranges": policy["ram_ranges"]} # Паспорт фиксирует аппаратный выбор и допустимые адреса приложения.
if can_profile and chosen["name"] == "M95M02": result["can_boot_profile"] = can_profile # Малая AT25256B не получает неподдерживаемый A/B-образ, даже если проект поддерживает второй аппаратный вариант.
return result # Prebuild сохраняет CAN-профиль, а postbuild проверяет его неизменность вместе с EEPROM и версией.
def load_identity(templates): # Загружает действующий генератор паспорта из явно выбранной библиотеки.
spec = importlib.util.spec_from_file_location("unified_identity_tools", templates / "c/firmware-info/tools/identity.py") # Обе схемы CCS используют одинаковый публичный контракт identity.
module = importlib.util.module_from_spec(spec) # Изолированный модуль не меняет импорты SETGUI и других проектов.
spec.loader.exec_module(module) # Ошибка отсутствующей зависимости возвращается вызывающему процессу до записи выпуска.
module.git_state = git_state # Локальное доверие Git ограничено выбранными рабочими деревьями без изменения глобального safe.directory.
return module # Подготовка и упаковка используют одну реализацию формата manifest.
def prepare(project, templates, output, eeprom=None, config=None): # Единый prebuild поддерживает исходный счётчик 166 и ручную версию 167.
project, templates, output = project.resolve(), templates.resolve(), output.resolve() # Все относительные пути фиксируются до запуска общих инструментов.
config = config.resolve() if config else project / "firmware_identity.json" # Старые BAT-вызовы могут явно передать прежний файл настроек.
settings = json.loads(config.read_text(encoding="utf-8-sig")) # Читает пользовательские поля без потери дополнительных настроек проекта.
policy, profile = read_policy(project), read_profile(project, templates, eeprom) # Физическая разметка проверяется до изменения счётчика версии.
identity = load_identity(templates) # Подключает генератор той библиотеки, которая включена в компиляцию.
if policy.get("version_header"): # Ручной заголовок остаётся единственным источником версии 167.
settings.update(version=configured_version(project, settings), increment_version=False) # Не увеличивает номер при повторном prebuild или ошибке компиляции.
with tempfile.TemporaryDirectory(prefix="c2000-identity-") as directory: # Временные настройки не переписывают пользовательский JSON.
temporary = Path(directory) / "config.json" # Временный файл нужен только публичному интерфейсу существующего генератора.
temporary.write_text(json.dumps(settings, ensure_ascii=False), encoding="utf-8") # Сохраняет ручной номер и все поля изделия в UTF-8.
metadata = identity.prepare(project, templates, temporary, output) # Генерирует прежние 12 слов паспорта без изменения протокола прошивки.
else: # Проект 166 продолжает резервировать следующий номер непосредственно в своём JSON.
metadata = identity.prepare(project, templates, config, output) # Итератор вызывается ровно один раз, сохраняя старый контракт precompile.
metadata.update(profile) # Предел EEPROM применяется и к старым проектам, и к новым аппаратным профилям.
if "eeprom_bytes" in profile: # Только явный аппаратный профиль требует сверки compiler define и происхождения разметки.
metadata["templates_root"] = str(templates) # Защищает postbuild от проверки по другой копии C-заголовков.
selection = policy["eeprom"] # Имена заголовка и макроса определены декларативным профилем платы.
value = next(key for key, item in selection["variants"].items() if item["name"] == profile["eeprom"]) # Вычисляет точное значение define для выбранной микросхемы.
guard = '\n#include "' + Path(selection["selection_header"]).name + '" /* Проверяет аппаратный профиль текущей компиляции. */\n' # Проверка включается в генерируемый C-заголовок паспорта.
guard += '#if ' + selection["selection_macro"] + ' != ' + value + ' /* Сверяет define компилятора с подготовленным паспортом. */\n#error EEPROM_profile_differs_from_prebuild /* Требует согласованную пересборку платы. */\n#endif /* Завершает проверку профиля EEPROM. */\n' # Compiler-only override не может незаметно изменить физическую разметку.
header = output / "firmware_identity.h" # Добавляет защиту после прежних констант версии и времени.
identity.write_atomic(header, header.read_text(encoding="utf-8") + guard) # Заголовок заменяется атомарно и всегда остаётся в UTF-8.
identity.write_atomic(output / "firmware_identity.json", json.dumps(metadata, ensure_ascii=False, indent=2) + "\n") # Сохраняет фактическую разметку для проверки после линковки.
return metadata # Публичный результат совместим с прежними host-тестами и проектными вызовами.
def convert(compiler, build_dir, artifact, metadata_path, stage, tools_templates): # Выполняет только конвертацию во временный каталог до общей проверки выпуска.
spec = importlib.util.spec_from_file_location("unified_c2000_converter", tools_templates / "c/firmware-info/tools/c2000_postbuild.py") # Использует единственную низкоуровневую реализацию hex2000 и сборки дополнительного Flash.
converter = importlib.util.module_from_spec(spec) # Изолирует импорт от старых командных точек входа и исключает рекурсивный postbuild.
sys.path.insert(0, str(tools_templates / "c/firmware-info/tools")) # Соседний identity нужен существующему конвертору при импорте.
try: # Любая ошибка конвертации остаётся внутри staged-выпуска.
spec.loader.exec_module(converter) # Загружает библиотечную функцию без запуска CLI.
source = Path(artifact) # CCS передаёт абсолютный базис OUT либо имя относительно активного BuildDirectory.
source = Path(str(source if source.is_absolute() else build_dir / source) + ".out") # Выбирает конкретный исходный файл текущей сборки.
with tempfile.TemporaryDirectory(prefix=".input-", dir=stage) as directory: # Дополнительные копии конвертора не попадают рядом с постоянным RAM OUT до проверки.
staged = Path(directory) / source.name # Имя артефакта сохраняется для штатного именования Flash и BIN.
shutil.copy2(source, staged) # Передаёт точные байты текущего COFF, сохраняя исходный файл.
converter.postbuild(compiler, build_dir, str(staged.with_suffix("")), metadata_path, stage) # Публикация и сеть отсутствуют: результаты ещё должны пройти проверки общего модуля.
finally: # Импорт другого проекта не должен наследовать временно добавленный каталог инструментов.
sys.path.pop(0) # Возвращает прежний порядок поиска Python-модулей даже при отказе TI.
def write_receipt(output, archive_name, image_name): # Передаёт Публикатору точную папку текущего проверенного выпуска.
value = {"schema": 1, "folder": archive_name, "manifest": image_name + ".manifest.json"} # Относительные имена сохраняют переносимость комплекта между компьютерами.
receipt = output / "latest_build.json" # Служебный указатель находится вне папки неизменяемого комплекта.
temporary = receipt.with_suffix(".tmp") # Атомарная замена не оставляет частичный JSON при прерывании записи.
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") # Записывает только локальный указатель без сетевой публикации.
temporary.replace(receipt) # Указатель меняется лишь после успешной проверки и копирования всех файлов.
def atomic_copy(source, destination): # Сохраняет проверенный файл без открытия старого результата на перезапись и без частичного содержимого.
destination = Path(destination) # Нормализует также строковые пути, переданные стандартным shutil.copytree.
with tempfile.NamedTemporaryFile(dir=destination.parent, prefix=".prepared-", delete=False) as stream: # Собственный временный файл наследует права каталога и находится на том же диске для атомарной замены.
temporary = Path(stream.name) # Запоминает только созданный текущим вызовом файл для безопасной очистки.
try: # Ошибка записи или замены должна оставить прежний результат пригодным для чтения.
shutil.copy2(source, temporary) # Сначала полностью записывает новые байты и сохраняет время исходного артефакта.
temporary.replace(destination) # Одной файловой операцией заменяет старый результат после успешной записи временного файла.
finally: # Незавершённый временный файл не должен попадать в будущий комплект Публикатора.
try: # Python 3.7 не поддерживает параметр missing_ok у pathlib.Path.unlink.
temporary.unlink() # Удаляет только собственный незавершённый файл при ошибке копирования или замены.
except FileNotFoundError: # Успешный replace уже перенёс файл на постоянное имя.
pass # Отсутствующий временный файл не превращает успешное копирование в ошибку сборки.
return str(destination) # Сохраняет контракт copy_function стандартного shutil.copytree.
def verify_embedded(image, metadata, flash=False): # RAM и Flash SCI8 проверяются отдельно по разным допустимым адресам и ограничениям загрузчика.
data = image.read_bytes() # Проверяет содержимое файла, которое будет передано в EEPROM-программатор.
limit = 0xFC00 * 2 - 2 if flash else metadata.get("max_image_bytes", 0xFFFFFFFF) # RS Flash использует внешний буфер 0x200000..0x20FC00 и ещё два октета CRC; RAM BIN ограничен резервом физической EEPROM.
if not 24 <= len(data) <= limit: # Учитывает заголовки SCI8 вместе с кодом, не позволяя переписать резерв параметров либо выйти за буфер RS.
raise ValueError("SCI8 exceeds EEPROM firmware boundary or is too short") # Отвергает слишком большой BIN до создания постоянного каталога выпуска.
if data[:2] != b"\xaa\x08": # 0x08AA — обязательный ключ восьмибитной boot-таблицы TI.
raise ValueError("Invalid SCI8 header") # Не принимает HEX, COFF либо плоский дамп под видом загрузочного BIN.
stamp = datetime.fromisoformat(metadata["built_at_utc"].replace("Z", "+00:00")) # Берёт момент именно того prebuild, который подготовил C-заголовок.
build_id = metadata["build_id"] # Контракт firmware-info v1 хранит ровно восемь ASCII-символов идентификатора.
if not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", build_id) or stamp.utcoffset() is None or stamp.utcoffset().total_seconds(): # Исключает неоднозначную дату и неподдерживаемый идентификатор.
raise ValueError("Invalid prebuild identity") # Требует заново выполнить prebuild вместо выпуска чужих метаданных.
words = [1, *metadata["version"], stamp.year, stamp.month * 256 + stamp.day, stamp.hour * 256 + stamp.minute, stamp.second] # Независимо восстанавливает первые восемь 16-битных регистров общего контракта.
words += [ord(build_id[index]) * 256 + ord(build_id[index + 1]) for index in range(0, 8, 2)] # Упаковывает две ASCII-октеты в каждое из четырёх слов идентификатора.
expected = struct.pack("<12H", *words) # TI SCI8 хранит каждое слово полезных данных младшей октетой вперёд.
high, low = struct.unpack_from("<HH", data, 18) # Читает точку входа после ключа и восьми резервных слов загрузчика.
entry = (high << 16) | low # Адрес C28x передаётся старшим словом перед младшим.
offset, found, blocks, boot, loaded = 22, 0, [], 0, {} # Запоминает диапазоны, boot-переход и точные загружаемые слова для сопоставления Flash COFF с SCI8.
while offset + 2 <= len(data): # Перед чтением размера требует наличие полного 16-битного слова.
count, = struct.unpack_from("<H", data, offset) # Длина блока задана в адресуемых словах C28x, а не физических октетах.
offset += 2 # Пропускает размер перед адресом блока или терминатором таблицы.
if not count: # Нулевое слово завершает SCI8 без последующего адресного поля.
if offset != len(data) or found != 1 or (flash and boot != 1) or not any(start <= entry < end for start, end in blocks): # Дополнительно требует единственный двухсловный аппаратный переход Flash boot.
raise ValueError(f"SCI8 identity/entry/terminator mismatch: matching identities={found}") # Сообщает отсутствие встроенного паспорта вместо ложной подписи старого OUT.
if flash: validate_words(entry, loaded) # CRC FAB1 и границы сектора A проверяются по фактическим октетам SCI8 до публикации.
return entry, loaded # Flash-проверка сопоставит реальные загрузочные слова с секциями исходного COFF; RAM-вызов может игнорировать результат.
if offset + 4 + count * 2 > len(data): # Требует полный 32-битный адрес и все октеты полезной нагрузки.
raise ValueError("Truncated SCI8 section") # Не допускает выпуск усечённой секции даже при паспорте в её начале.
high, low = struct.unpack_from("<HH", data, offset) # Получает адрес назначения текущего блока в словах C28x.
address, end = (high << 16) | low, ((high << 16) | low) + count # Конец исключается из диапазона, поэтому соседние блоки могут соприкасаться.
allowed = (APP_START <= address < end <= APP_STOP) if flash else any(start <= address < end <= stop for start, stop in metadata.get("ram_ranges", ((0, 0x100000000),))) # Flash-приложение целиком находится ниже сектора A; RAM A/B по-прежнему ограничено своим payload-диапазоном.
if not allowed or any(address < old_end and start < end for start, old_end in blocks): # Любое перекрытие секций либо несовместимый тип таблицы блокирует выпуск.
raise ValueError("SCI8 block outside application RAM or overlapping") # Не выпускает Flash-таблицу как EEPROM/RAM-образ.
if len(blocks) >= metadata.get("max_sections", 0xFFFFFFFF): # CAN-порт F28335 принимает не более 64 секций; обычные RAM/Flash-профили сохраняют прежний контракт.
raise ValueError("SCI8 exceeds bootloader section count") # Совместимый размер файла не разрешает таблицу, которую аппаратный загрузчик не сможет разобрать.
blocks.append((address, end)) # Сохраняет проверенный диапазон для контроля следующих блоков и точки входа.
payload = data[offset + 4:offset + 4 + count * 2] # Ищет паспорт только внутри данных, исключая поля заголовка и адреса.
boot += int(flash and address == APP_START and count >= 16) # Ровно один блок содержит полный FAB1; аппаратный boot 0x33FFF6 запрещён.
loaded.update((address + index, value) for index, value in enumerate(struct.unpack("<" + "H" * count, payload))) # Фиксирует реальные 16-битные слова, которые RS-загрузчик запрограммирует во Flash.
found += sum(payload[index:index + 24] == expected for index in range(0, len(payload) - 23, 2)) # Учитывает лишь выровненные на слово экземпляры 12 регистров firmware-info.
offset += 4 + count * 2 # Переходит за полный адрес и полезные слова к следующему размеру.
raise ValueError("Missing SCI8 terminator") # Исчерпание файла без нулевого слова никогда не считается успешной конверсией.
def verify_package(image, metadata): # Связывает BIN, manifest и SHA-256 с тем же prebuild и аппаратной EEPROM.
manifest = json.loads(image.with_suffix(image.suffix + ".manifest.json").read_text(encoding="utf-8")) # Один контракт SHA/паспорта обслуживает отдельные RAM BIN, Flash COFF и Flash SCI8.
data = image.read_bytes() # Повторно считывает точные октеты перед постоянной публикацией.
digest = hashlib.sha256(data).hexdigest() # Вычисляет контрольную сумму независимо от общего seal.
if manifest.get("file") != image.name or manifest.get("size_bytes") != len(data) or manifest.get("sha256") != digest: # Требует согласованности имени, размера и содержимого файла.
raise ValueError("BIN does not match its manifest") # Не публикует повреждённый либо перепутанный комплект.
if any(manifest.get(key) != value for key, value in metadata.items()): # Проверяет также eeprom, физическую границу, версию и происхождение компилируемых templates.
raise ValueError("Manifest does not match prebuild identity/EEPROM") # Один лишь корректный SHA не разрешает паспорт другого аппаратного варианта.
if image.with_suffix(image.suffix + ".sha256").read_text(encoding="utf-8").strip() != f"{digest} {image.name}": # Общий sidecar содержит SHA и точное имя через два пробела.
raise ValueError("Invalid BIN SHA-256 sidecar") # Требует целостности всех трёх публикуемых файлов.
def verify_linker_profile(map_path, profile): # Сверяет аппаратную настройку Python с фактическими абсолютными символами линкера.
text = map_path.read_text(encoding="utf-8-sig") # MAP текущей конфигурации является обязательным результатом успешной линковки.
expected = {"_EEPROM_TOTAL_BYTES": profile["eeprom_bytes"], "_EEPROM_FIRMWARE_LIMIT_BYTES": profile["max_image_bytes"], "_EEPROM_PARAMS_START_WORD": profile["max_image_bytes"] // 2} # Символы разделяют физические октеты и адресуемые слова C28x.
for name, value in expected.items(): # Проверяет ёмкость, конец прошивки и начало сохраняемых параметров до запуска hex2000.
values = re.findall(r"^\s*(?:\w+\s+)?([0-9a-fA-F]{8})\s+" + re.escape(name) + r"\b", text, re.M) # Принимает формат глобальных TI-символов с номером страницы либо признаком ABS.
if not values or any(int(item, 16) != value for item in values): # Повторные списки MAP обязаны описывать один и тот же профиль.
raise ValueError(f"Linker EEPROM profile mismatch: {name}") # Выявляет define, переданный только компилятору или только линкеру, и устаревшую карту.
def verify_flash_coff(image, compiler): # Проверяет реальные секции COFF официальным TI-декодером и возвращает точные Flash-слова загрузки.
tool = compiler / "bin/ofd2000.exe" # Разбирает формат тем же установленным C2000 toolchain, который создал OUT.
document = ET.fromstring(subprocess.check_output([str(tool), "-x", str(image)], stderr=subprocess.STDOUT)) # XML содержит фактические адреса и двоичные данные секций, а не неподтверждённую соседнюю MAP.
coff = document.find(".//ti_coff") # Вариант предназначен строго для COFF ABI F28335.
if coff is None or coff.findtext("file_header/target_id") != "0x9d" or coff.findtext("file_header/exec") != "true": # Не принимает объект другого процессора, ELF либо незалинкованный модуль.
raise ValueError("Expected executable C2000 COFF Flash image") # Явно отделяет JTAG Flash OUT от обычного RAM OUT и чужих архитектур.
sections = {section.findtext("name"): section for section in coff.findall("section")} # Именованные выходные секции соответствуют отдельной Flash-карте линкера 167.
required = {".text": (APP_DATA, APP_STOP, APP_DATA, APP_STOP), ".flash_app": (APP_START, APP_DATA, APP_START, APP_DATA), "flash_rs": (0x8000, 0xC000, APP_DATA, APP_STOP), "ramfuncs": (0xC000, 0xD000, APP_DATA, APP_STOP)} # FAB1 находится в секторе H; все Flash LOAD-секции исключают постоянный сектор A, сохраняя прежние RUN-области RAM.
for name, (run_begin, run_end, load_begin, load_end) in required.items(): # Наличие автономного RAM-загрузчика и InitFlash обязательно для выпуска Flash-варианта.
section = sections.get(name) # Отсутствующая обязательная секция не маскируется другими именами или символами MAP.
if section is None: # Проверяет отдельную карту и startup, а не одно лишь расположение основного кода.
raise ValueError("Missing Flash COFF section: " + name) # Не выдаёт RAM COFF за пригодный для автономной Flash.
run, load, size = (int(section.findtext(field, "0"), 0) for field in ("physical_addr", "virtual_addr", "raw_data_size")) # Длина OFD задана физическими октетами, оба адреса — 16-битными словами CPU.
if not size or size % 2 or not run_begin <= run < run + size // 2 <= run_end or not load_begin <= load < load + size // 2 <= load_end or (name == ".flash_app" and size != 32): # FAB1 занимает ровно 16 слов; прочие секции целиком помещаются в разрешённых LOAD/RUN-областях ниже сектора A.
raise ValueError("Invalid Flash COFF LOAD/RUN section: " + name) # Отвергает stale linker, размещение API во Flash во время стирания и аппаратный вход вне 0x33FFF6.
loaded = {} # Сопоставляет слова всех загружаемых Flash-секций с будущей SCI8-таблицей RS.
for section in sections.values(): # Служебные debug/copy-секции адреса ноль в программирование не входят.
address = int(section.findtext("virtual_addr", "0"), 0) # Для COFF TI это загрузочный адрес содержимого секции.
raw = section.findtext("raw_data") # Пустая .ebss, NOLOAD или DSECT не задаёт байтов прошивки.
if raw is None or not 0x300000 <= address < 0x340000: # Не включает отладочные записи, ROM-таблицы и рабочую RAM в образ Flash.
continue # Права оставшихся загружаемых Flash-слов дополнительно проверит общий SCI8-валидатор.
data = base64.b64decode(raw) # Получает именно те октеты, которые записаны в COFF и должны оказаться в Flash SCI8.
if len(data) % 2: # Любая программируемая секция C28x должна содержать целые 16-битные слова.
raise ValueError("Odd Flash COFF section size") # Не допускает неоднозначную упаковку образа RS.
for index, value in enumerate(struct.unpack("<" + "H" * (len(data) // 2), data)): # Восстанавливает little-endian представление каждого физического слова COFF.
if address + index in loaded: # Два выходных диапазона не должны программировать одно слово повторно.
raise ValueError("Overlapping Flash COFF load sections") # Проверяет реальный OUT независимо от доверия к линкерной карте.
loaded[address + index] = value # Сохраняет ожидаемые данные для независимого сравнения с hex2000.
entry = int(coff.findtext("optional_file_header/entry_point", "0"), 0) # Получает реальную точку входа C runtime из заголовка COFF.
if entry not in loaded or not 0x300000 <= entry < 0x33FF80: # C startup должен находиться в полностью загружаемой исполняемой Flash.
raise ValueError("Flash COFF entry is not loaded") # Исключает OUT со старой RAM-точкой входа или отрезанным startup.
checked_entry, checked_loaded, _ = coff_sections(document) # Общий декодер дополнительно отвергает любой загружаемый RAW вне области приложения, включая OTP и RAM.
if checked_entry != entry or checked_loaded != loaded: raise ValueError('Flash COFF load data mismatch') # Ни одна секция не должна быть потеряна при независимом обходе OFD XML.
validate_words(entry, loaded) # Проверяет FAB1 и CRC по полному согласованному COFF-образу.
return entry, loaded # Полное сравнение с SCI8 обнаружит конверсию другого OUT, потерю секции или подмену одного слова.
def verify_flash_release(stage, artifact, metadata, compiler, identity, required=False): # Проверяет и отдельно маркирует Flash COFF/SCI8 в staged-выпуске до любого постоянного копирования.
base = Path(artifact.replace("\\", "/")).name # Использует исходное имя артефакта CCS без директории и расширения OUT.
coff, serial = stage / (base + "_flash.out"), stage / (base + "_flash_rs.bin") # JTAG COFF и RS SCI8 являются разными формами одного отдельного Flash-варианта.
if not coff.exists() and not serial.exists() and not required: # Проекты без build_flash.py и host-фикстуры сохраняют прежний защищённый выпуск RAM.
return # Наличие обычного RAM BIN само по себе не объявляет поддержку Flash.
if not coff.is_file() or not serial.is_file(): # Сборка Flash должна выпустить оба согласованных артефакта.
raise ValueError("Incomplete Flash COFF/SCI8 release") # Не экспортирует один старый OUT вместе с новым RAM-выпуском.
verify_linker_profile(coff.with_suffix(".map"), metadata) # Flash использует физическую EEPROM для параметров и не меняет её профиль при размещении кода внутри CPU.
if verify_flash_coff(coff, compiler) != verify_embedded(serial, metadata, flash=True): # Сравнивает entry и каждое загрузочное слово официального COFF с результатом hex2000.
raise ValueError("Flash SCI8 does not match its COFF") # Отклоняет stale конверсию даже при правильных независимых SHA-файлах.
pairs = [(path, "COFF") for path in stage.rglob(coff.name)] + [(path, "SCI8-FLASH") for path in stage.rglob(serial.name)] # Включает плоские результаты и все их копии внутри текущего архива.
for path, kind in pairs: # Сначала проверяет весь исходный комплект, не меняя даже временных паспортов при первом отказе.
verify_package(path, metadata) # Общий postcompile обязан сохранить текущие version/EEPROM/SHA для каждого результата.
if path.read_bytes() != (coff if kind == "COFF" else serial).read_bytes(): # Архивированная копия должна быть побайтно тем же проверенным вариантом.
raise ValueError("Flash archive differs from staged release") # Не позволяет экспортировать чужой Flash внутри корректного RAM-архива.
with tempfile.TemporaryDirectory(prefix=".variant-", dir=stage) as directory: # Вариантные JSON нужны только для подписи разных форматов и не попадают в постоянный выпуск.
metadata_path = Path(directory) / "metadata.json" # Общий seal принимает обычный путь и не требует изменения соседнего templates.
for path, kind in pairs: # Только проверенные Flash-файлы получают явный признак исполнения и транспортный формат.
specific = dict(metadata, execution="FLASH", image_format=kind, boot_contract="f28335-universal-v1", boot_protected=True, flash_app_start=APP_START, flash_app_stop=APP_STOP) # Паспорт явно отличает защищённое приложение от старого самостоятельного Flash; аппаратная EEPROM сохраняется отдельно.
metadata_path.write_text(json.dumps(specific, ensure_ascii=False), encoding="utf-8") # Сохраняет все поля происхождения и ручную версию из общего prebuild.
identity.seal(path, metadata_path) # Переподписывает конкретный COFF либо SCI8 с явным execution=FLASH только после проверки байтов.
verify_package(path, specific) # Подтверждает согласованность готового вариантного manifest и SHA перед публикацией.
def create_can_release(stage, image, metadata, map_path, identity): # Добавляет отдельный CAN BIN только после проверки текущего RAM-образа и его интеграции с загрузчиком.
if not metadata.get("can_boot_profile"): return None # Старые проекты и AT25256B продолжают выпускаться без дополнительного неподдерживаемого файла.
if metadata["can_boot_profile"] != "f28335-m95m02-ab" or metadata.get("target") != "TMS320F28335" or metadata.get("eeprom") != "M95M02": # Профиль связан с конкретными процессором, памятью и A/B-портом.
raise ValueError("CAN profile requires TMS320F28335 with M95M02") # Запрещает переименование произвольного RAM либо Flash BIN в пригодный CAN-образ.
limits = dict(metadata, max_image_bytes=130560, ram_ranges=[[0x8000, 0xE000]], max_sections=64) # Ограничения взяты из f28335_boot_store: длина в октетах, адреса в 16-битных словах CPU.
_entry, loaded = verify_embedded(image, limits) # Проверяет SCI8, паспорт, entry, все секции, перекрытия и защиту RAM загрузчика.
symbols = map_path.read_text(encoding="utf-8", errors="replace") # Использует MAP именно активной линковки, уже проверенный по профилю EEPROM.
for name in ("_F28335CanBoot_Init", "_F28335CanBoot_Ready", "_F28335CanBoot_Service", "_F28335CanBoot_RxISR"): # CAN-приложение должно содержать инициализацию, готовность, основной сервис и обработчик приёма.
matches = re.findall(r"^\s*\d+\s+([0-9a-fA-F]{8})\s+" + re.escape(name) + r"\s*$", symbols, re.M) # Читает адреса экспортированных TI-символов, исключая простые упоминания имени в описании секции.
if not matches or any(int(address, 16) not in loaded for address in matches): # Требует присутствие hook в загружаемом payload текущего BIN, а не только в произвольном MAP.
raise ValueError("CAN application hook missing from SCI8/MAP: " + name) # Старое приложение без READY/CONFIRM нельзя выпускать как автоматически пригодное для CAN A/B.
specific = dict(metadata, execution="RAM", image_format="SCI8-RAM", transport="can", protocol="ProtoCAN Boot", product_type=0x2833, hardware_min=1, hardware_max=1) # Паспорт отделяет CAN от RS Flash и сохраняет прежние ручную версию, SHA и аппаратную идентичность.
can_image = image.with_name(image.stem + "_can.bin") # Отдельный суффикс однозначно показывает назначение файла, сохраняя имя артефакта и Build ID.
can_image.write_bytes(image.read_bytes()) # По CAN передаётся та же полная SCI8-таблица RAM без нового заголовка или изменения адресов секций.
with tempfile.TemporaryDirectory(prefix=".can-metadata-", dir=stage) as directory: # Вариантный паспорт не подменяет исходные метаданные компиляции.
metadata_path = Path(directory) / "metadata.json" # Общий seal читает отдельный временный JSON для конкретного транспортного файла.
metadata_path.write_text(json.dumps(specific, ensure_ascii=False), encoding="utf-8") # Русские поля пользовательского паспорта сохраняются в UTF-8.
identity.seal(can_image, metadata_path) # Создаёт собственные manifest и SHA-256 с точным именем CAN BIN.
verify_package(can_image, specific) # До публикации сверяет все поля нового паспорта и точные байты файла.
archive = stage / identity.build_folder_name(metadata) # Дополняет тот же архив текущей сборки, который уже содержит обычный RAM и варианты Flash.
if not archive.is_dir(): raise ValueError("CAN release requires the current build archive") # Не создаёт отдельный неполный выпуск, который не попадёт в штатный экспорт GUI.
for file in (can_image, can_image.with_suffix(".bin.manifest.json"), can_image.with_suffix(".bin.sha256")): # Все три файла должны одновременно попасть в плоский выпуск и архив.
shutil.copy2(file, archive / file.name) # Копирование ещё выполняется внутри временного staged-комплекта без изменения предыдущих выпусков.
verify_package(archive / can_image.name, specific) # Проверяет архивную копию до копирования в output и каталог GUI.
return can_image # Вызывающий выводит точное имя только после успешного завершения всего защищённого postbuild.
def arrange_release(stage, archive_name, artifact, image_name, can_image, target=None): # Получает target проверенного паспорта; необязательный параметр сохраняет прежнюю раскладку старых вызовов.
primary_purpose = "flash" if target == "TMS320F2812" else "eeprom" # F2812 хранит основной SCI8-образ во внешней параллельной Flash; для остальных целей сохраняем EEPROM.
base = Path(artifact.replace("\\", "/")).name # Использует базовое имя CCS, включая случай абсолютного BuildArtifactFileBaseName.
routes = {base + "_flash.out": "flash", base + "_flash.map": "flash", base + "_flash_rs.bin": "rs"} # OUT и карта относятся к JTAG, а отдельный SCI8 Flash BIN передаётся последовательным загрузчиком.
if can_image is not None: routes[can_image.name] = "can" # CAN получает только вариант, прошедший ограничения A/B-слота и проверку прикладных hooks.
shutil.copy2(stage / archive_name / "firmware_identity.json", stage / "firmware_identity.json") # Общий паспорт связывает четыре папки как в текущем локальном выпуске CCS, так и в его неизменяемом архиве.
for folder in (stage / archive_name, stage): # Сначала перестраивает архив, затем плоские результаты того же временного выпуска.
for file in tuple(folder.iterdir()): # Снимок списка исключает повторную обработку создаваемых папок назначения.
if not file.is_file() or file.name == "firmware_identity.json": continue # Архив и исходный общий паспорт остаются на своём уровне без дублирования между назначениями.
purpose = next((route for name, route in routes.items() if file.name == name or file.name.startswith(name + ".")), primary_purpose) # Manifest, SHA, карты и основной OUT/HEX следуют назначению данного процессора; отдельные варианты сохраняют свои маршруты.
destination = folder / purpose # Одинаковые четыре назначения применяются к локальному CCS-выпуску и экспортируемому архиву.
destination.mkdir(exist_ok=True) # Пустая папка неподдержанного CAN/Flash не создаётся и не обещает отсутствующего образа.
file.replace(destination / file.name) # Перемещает только внутри временного stage, не затрагивая ранее опубликованные файлы.
return (Path(primary_purpose) / image_name).as_posix() # Указатель последней сборки и экспорт получают фактическую папку основного BIN, включая flash для F2812.
def publish(project, templates, tools_templates, compiler, build_dir, artifact, map_path, output, export=None, eeprom=None): # Выпускает только проверенные результаты конкретной конфигурации CCS.
project, templates, tools_templates = project.resolve(), templates.resolve(), tools_templates.resolve() # Фиксирует выбранные исходники и инструменты независимо от рабочего каталога терминала.
build_dir, compiler = build_dir.resolve(), compiler.resolve() # Общая BAT-точка получает полные пути текущих компилятора и Debug/Release.
metadata_path = build_dir / "generated/firmware_identity.json" # Использует паспорт, подготовленный до компиляции этой конфигурации.
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 от постоянных каталогов выпуска.
convert(compiler, build_dir, artifact, metadata_path, stage, tools_templates) # Любая ошибка TI или общей библиотеки прерывает публикацию.
image_name = Path(artifact.replace("\\", "/")).name + "_" + metadata["build_id"] + ".bin" # Повторяет штатное имя конкретного образа, не сканируя старые BIN.
image = stage / image_name # Проверяет только результат текущего вызова конвертора.
verify_embedded(image, metadata) # Обязательные проверки EEPROM, полной SCI8-таблицы и реально встроенных 12 слов.
verify_package(image, metadata) # Сверяет SHA и весь паспорт, включая физический профиль EEPROM.
for archived in stage.rglob(image_name): # Проверяет также копии внутри текущего архива перед их публикацией или export.
verify_package(archived, metadata) # Архив обязан содержать те же байты и профиль, что плоский выпуск.
spec = importlib.util.spec_from_file_location("release_identity_tools", tools_templates / "c/firmware-info/tools/identity.py") # Подключает существующий формат именования и экспорта архива без копирования реализации.
identity = importlib.util.module_from_spec(spec) # Создаёт отдельный модуль инструментов, не подменяя компилируемую зависимость паспорта.
spec.loader.exec_module(identity) # Ошибка подключения остаётся внутри временного этапа до постоянных файлов.
verify_flash_release(stage, artifact, metadata, compiler, identity, required=(project / "build_flash.py").is_file()) # Отдельная Flash-сборка требует собственного безопасного COFF/SCI8 и не ослабляет прежние RAM-проверки.
can_image = create_can_release(stage, image, metadata, map_path, identity) # Автоматически создаёт и проверяет CAN-вариант до переноса любого результата в постоянный выпуск.
archive_name = identity.build_folder_name(metadata) # Сохраняет принятые имена архивов с ревизией, признаком изменений и временем UTC+3.
if not (stage / archive_name / image_name).is_file(): # Полный архив является обязательным результатом общей конверсии, а не необязательной старой папкой.
raise ValueError("Common postcompile did not produce the current build archive") # Не публикует неполный комплект без OUT, MAP и паспорта текущего выпуска.
image_name = arrange_release(stage, archive_name, artifact, image_name, can_image, target=metadata["target"]) # Выбираем папку по проверенному target текущей компиляции до копирования в проект и каталог экспорта.
output = output.resolve() # Постоянный каталог появляется только после всех проверок staged-комплекта.
output.mkdir(parents=True, exist_ok=True) # Прежние проверенные выпуски сохраняются при добавлении нового.
for item in sorted(stage.iterdir(), key=lambda item: item.is_dir()): # Переносит только файлы и архив, созданные данным postcompile.
if item.is_dir(): # Архив содержит сопроводительные результаты той же сборки.
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) # Подтверждает отсутствие повреждения при копировании на постоянный диск.
if export is not None: # Копирование в каталог GUI выполняется последним и лишь для прошедшего проверки архива.
identity.export_build(output / archive_name, export.resolve(), image_name) # Использует общий экспорт уже проверенного локального выпуска.
print(f"[firmware-build] Verified {profile.get('eeprom', settings['target'])} BIN: {output / image_name}") # Показывает пригодный аппаратный профиль и точный путь конечного файла.
if can_image is not None: print(f"[firmware-build] CAN BIN: {output / 'can' / can_image.name}") # После успешного выпуска показывает файл из отдельной папки ProtoCAN.
write_receipt(output, archive_name, image_name) # Фиксирует точную папку выпуска для импорта Публикатором без поиска по времени.
return output / image_name # Даёт тестам и внешним средствам однозначный результат без поиска по времени.
def main(argv=None): # Общая точка входа CCS и старых BAT: подготовка и проверка не выполняют сетевую публикацию.
parser = argparse.ArgumentParser(description="Единая подготовка C2000 для CCS и Публикатора SETGUI") # Показывает назначение обеих команд пользователю терминала.
parser.add_argument("command", choices=("prepare", "build")) # Разделяет резервирование версии до компиляции и проверку готового OUT после неё.
for name in ("project", "templates", "tools-templates", "config", "compiler", "build-dir", "metadata", "map", "output", "export"): # Все пути передаются явно или выводятся из публичных соглашений CCS.
parser.add_argument("--" + name, type=Path) # Path исключает интерпретацию аргументов через командную оболочку.
parser.add_argument("--artifact") # Базовое имя OUT передаётся без расширения как штатный макрос CCS.
parser.add_argument("--eeprom", choices=("AT25256B", "M95M02")) # Сохраняет существующий явный выбор тестового аппаратного профиля.
args = parser.parse_args(argv) # Ошибки аргументов завершают процесс до любых изменений версии или файлов выпуска.
tools = Path(__file__).resolve().parents[3] # В полной библиотеке модуль расположен в c/firmware-info/tools.
project = (args.project or (args.build_dir.parent if args.build_dir else Path.cwd())).resolve() # Старый postcompile определяет проект по родителю Debug/Release.
config = args.config or project / "firmware_identity.json" # Старый JSON 166 остаётся совместимым без обязательной миграции.
if args.command == "build" and not config.is_file() and args.build_dir: # Прямой postcompile допускает подготовленный паспорт без отдельного пользовательского JSON.
config = args.metadata or args.build_dir / "generated/firmware_identity.json" # В таком случае доступные проверки определяются реальными метаданными компиляции.
settings = json.loads(config.read_text(encoding="utf-8-sig")) # Изделие и зависимости определяются настройками проекта, а не текущим каталогом.
templates = args.templates or (config.parent / settings["templates"] if settings.get("templates") else tools) # Поддерживает относительный путь общей библиотеки старых проектов.
if args.command == "build" and not args.templates and args.build_dir: # Старый BAT postcompile не передаёт отдельный путь компилируемых исходников templates.
previous = json.loads((args.metadata or args.build_dir / "generated/firmware_identity.json").read_text(encoding="utf-8")) # Использует точный путь, зафиксированный prebuild активной конфигурации.
templates = Path(previous.get("templates_root", templates)) # Проверки EEPROM читают те же заголовки даже при отдельном полном checkout инструментов.
try: # Возвращает CCS ненулевой код при отказе любой проверки.
if args.command == "prepare": # Prebuild вызывается ровно один раз перед компиляцией паспорта.
prepare(project, templates, args.output or args.build_dir / "generated", args.eeprom, config) # При ручной версии исходный JSON не меняется.
else: # Postbuild получает уже подготовленный паспорт текущей конфигурации.
if not args.compiler or not args.build_dir or not args.artifact: # Нельзя подменить отсутствующий артефакт произвольным старым файлом из каталога.
parser.error("build requires --compiler, --build-dir and --artifact") # Показывает обязательные параметры до запуска TI.
metadata = args.build_dir / "generated/firmware_identity.json" # Основное расположение совпадает с прежними конфигурациями CCS.
if args.metadata and args.metadata.resolve() != metadata.resolve(): # Защищённый выпуск использует только паспорт активной конфигурации.
raise ValueError("metadata must belong to the selected build directory") # Не принимает паспорт другой сборки с совпадающим номером версии.
image = Path(args.artifact) # Путь карты по умолчанию находится рядом с выбранным OUT.
image = image if image.is_absolute() else args.build_dir / image # Относительные артефакты разрешаются от BuildDirectory.
publish(project, templates, args.tools_templates or tools, args.compiler, args.build_dir, args.artifact, args.map or image.with_suffix(".map"), args.output or args.build_dir / "firmware", args.export, args.eeprom) # Подготовленный комплект передаётся Публикатору отдельно от сетевых действий.
except (OSError, ValueError, KeyError, TypeError, struct.error, subprocess.CalledProcessError) as error: # Ошибки формата, зависимостей и компилятора не скрываются успешным завершением сборки.
parser.exit(1, f"[firmware-build] ERROR: {error}\n") # Краткая причина остаётся видна в Console CCS.
return 0 # Успех подтверждает подготовку проверенного локального комплекта.
if __name__ == "__main__": # Импорт из проектного адаптера или тестов не запускает сборку.
raise SystemExit(main()) # Возвращает командной оболочке фактический результат подготовки.
+119
View File
@@ -0,0 +1,119 @@
"""Convert a CCS C2000 OUT to SCI8 HEX/BIN and seal its pre-build identity.
Uses only the standard library, TI hex2000 and the shared identity module.
No network, SETGUI, or project-specific paths are required.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import re
import subprocess
import sys # Запускает проектный сборщик тем же Python, которым выполняется общий postbuild.
import tempfile
from identity import archive_build, seal, verify
def build_flash_variant(compiler: Path, build_dir: Path, source: Path, stage: Path) -> tuple: # Возвращает файлы дополнительного Flash-образа только для проектов с явным сборщиком.
builder = build_dir.resolve().parent / "build_flash.py" # Соглашение CCS: Debug/Release расположены рядом с проектным сценарием сборки Flash.
if not builder.is_file(): # Отсутствие сценария оставляет прежний выпуск EEPROM без дополнительных требований к проекту.
return () # Пустой список не добавляет Flash-файлы в архив обычного проекта.
image = stage / (source.stem + "_flash.out") # Отдельное имя и временный каталог исключают подмену RAM-образа и использование устаревшего Flash-файла.
subprocess.run([sys.executable, str(builder), "--postbuild", "--compiler", str(compiler.resolve()), "--build-dir", str(build_dir.resolve()), "--output", str(image)], cwd=build_dir, check=True) # Режим postbuild использует уже собранные объекты и запрещает повторный запуск Make.
files = (image, image.with_suffix(".map")) # Карта линковки сопровождает COFF и позволяет проверить адрес аппаратной точки входа.
for file in files: # Оба результата должны быть выпущены текущим вызовом сборщика.
if not file.is_file() or not file.stat().st_size: # Успешный код процесса без непустого результата не считается успешной сборкой.
raise ValueError(f"Flash builder produced no file: {file}") # Ошибка останавливает postbuild до архивирования или экспорта неполного выпуска.
return files # Передаёт проверенные файлы вызывающему шагу для подписи и сохранения.
def postbuild(compiler: Path, build_dir: Path, artifact: str,
metadata: Path, output: Path, export: Path = None) -> Path:
# CCS may include the configured output directory in this macro.
# Resolve the input separately; output filenames must use only its basename.
artifact_path = Path(artifact)
if not artifact or artifact_path.name in ("", ".", ".."):
raise ValueError("artifact must name an OUT file without its extension")
artifact = artifact_path.name
if any(c in artifact for c in '\\/:*?"<>|'):
raise ValueError("invalid artifact file base name")
source_base = (artifact_path if artifact_path.is_absolute()
else build_dir / artifact_path)
source = Path(str(source_base) + ".out").resolve()
tool = compiler.resolve() / "bin" / "hex2000.exe"
if not tool.is_file():
raise FileNotFoundError(f"C2000 Hex Utility not found: {tool}")
if not source.is_file():
raise FileNotFoundError(f"compiled OUT not found: {source}")
metadata = metadata.resolve()
settings = json.loads(metadata.read_text(encoding="utf-8"))
build_id = settings["build_id"]
if not isinstance(build_id, str) or not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", build_id):
raise ValueError("invalid pre-build identity; run precompile before building")
if build_id == "NOGIT000":
print("[firmware-build] Git identity unavailable: using NOGIT000")
output = output.resolve()
output.mkdir(parents=True, exist_ok=True)
# Keep failed conversions and incomplete manifests out of the final folder.
with tempfile.TemporaryDirectory(prefix=".firmware-", dir=output) as temporary:
stage = Path(temporary)
hex_image = stage / (artifact + ".hex")
bin_image = stage / (artifact + "_" + build_id + ".bin")
flash_files = build_flash_variant(compiler, build_dir, source, stage) # Сначала собирает дополнительный Flash-вариант; его ошибка не допускает выпуск нового BIN.
for image, mode in ((hex_image, "-i"), (bin_image, "-b")):
subprocess.run([str(tool), str(source), "-boot", "-sci8",
"-map", str(image.with_suffix(image.suffix + ".map")),
"-o", str(image), mode], check=True)
if not image.is_file() or not image.stat().st_size:
raise ValueError(f"hex2000 produced no image: {image}")
seal(bin_image, metadata)
verify(bin_image, bin_image.with_suffix(bin_image.suffix + ".manifest.json"))
if flash_files: # Проекты со сборщиком получают независимую проверку точных байтов Flash-образа.
seal(flash_files[0], metadata) # Использует тот же паспорт prebuild для второго варианта этой сборки.
verify(flash_files[0], flash_files[0].with_suffix(".out.manifest.json")) # Проверяет размер и SHA-256 перед сохранением результатов.
serial_image = stage / (artifact + "_flash_rs.bin") # Отдельный SCI8 BIN содержит Flash-адреса и предназначен только для новой команды записи по RS.
serial_map = serial_image.with_suffix('.bin.map') # Карта конверсии позволяет отличить Flash-адреса от EEPROM/RAM BIN.
subprocess.run([str(tool), str(flash_files[0]), '-boot', '-sci8', '-map', str(serial_map), '-o', str(serial_image), '-b'], check=True) # Конвертирует именно Flash COFF с загрузочными адресами секций.
if not serial_image.is_file() or not serial_image.stat().st_size: # Пустая конверсия не должна попасть в выпуск либо каталог GUI.
raise ValueError(f'hex2000 produced no Flash RS image: {serial_image}') # Передаёт ошибку конверсии общему postcompile.
seal(serial_image, metadata) # Привязывает RS BIN к той же версии, ревизии и времени текущего prebuild.
verify(serial_image, serial_image.with_suffix('.bin.manifest.json')) # Проверяет точные байты перед копированием в Bin и архив.
flash_files += (serial_image, serial_map, serial_image.with_suffix('.bin.manifest.json'), serial_image.with_suffix('.bin.sha256')) # Включает транспортный BIN, карту и контрольные файлы в общий выпуск Flash.
for file in flash_files: # Сохраняет COFF и карту также рядом с обычным OUT для выбора в CCS.
destination = source.parent / file.name # Каталог исходного OUT задаёт место назначения без путей конкретного изделия в templates.
destination.write_bytes(file.read_bytes()) # Копирует только результаты успешного текущего вызова, сохраняя исходный RAM-образ.
for item in stage.iterdir():
item.replace(output / item.name)
result = output / bin_image.name # BIN остаётся основным образом EEPROM и возвращаемым результатом общего postbuild.
flash_archive = tuple(output / file.name for file in flash_files) # После переноса из временного каталога берёт актуальные Flash-файлы из firmware.
if flash_files: # Добавляет сопроводительные файлы только при выполненной сборке Flash.
flash_archive += (output / (flash_files[0].name + ".manifest.json"), output / (flash_files[0].name + ".sha256")) # Архив и GUI получают отдельный паспорт и контрольную сумму Flash COFF.
print(f"[firmware-build] Flash OUT: {source.parent / flash_files[0].name}") # Показывает пользователю CCS конкретный файл для прошивки внутренней Flash через JTAG.
archive_build(result, metadata, output, ( # Сохраняет файлы текущей сборки в архиве и передаёт весь архив в каталог GUI.
source, # Копирует исходный COFF-файл OUT с символами отладки под его именем для загрузки через JTAG из GUI.
output / hex_image.name,
output / (hex_image.name + ".map"),
output / (bin_image.name + ".map"), *flash_archive), export=export) # Экспортирует EEPROM и Flash в одной папке текущего выпуска.
print(f"[firmware-build] BIN: {result}")
print(f"[firmware-build] Manifest: {result}.manifest.json")
print(f"[firmware-build] SHA-256: {result}.sha256")
return result
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
for name in ("compiler", "build-dir", "metadata", "output"):
parser.add_argument("--" + name, type=Path, required=True)
parser.add_argument("--artifact", required=True)
parser.add_argument("--export", type=Path)
try:
postbuild(**vars(parser.parse_args()))
except (OSError, ValueError, KeyError, TypeError, subprocess.CalledProcessError) as error:
parser.exit(1, f"[firmware-build] ERROR: {error}\n")
return 0
if __name__ == "__main__":
raise SystemExit(main())
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.
+130 -8
View File
@@ -7,12 +7,21 @@ protocol encoding lives here: the generated header contains scalar constants.
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
import hashlib
import importlib.util # Загружает общий контракт JSON без импорта протокольного runtime и без зависимости от установленного пакета.
import json
import os
import re
import shutil
import tempfile
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:
@@ -36,20 +45,47 @@ def git_state(repository: Path) -> dict:
def write_atomic(path: Path, text: str):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(path.name + ".tmp")
temporary.write_text(text, encoding="utf-8", newline="\n")
# Built-in open supports newline on Python versions before 3.10.
with open(temporary, "w", encoding="utf-8", newline="\n") as stream:
stream.write(text)
temporary.replace(path)
def next_version(version): # Вычисляет следующий номер по ограничениям общего контракта firmware-info v1.
result = list(version) # Сохраняет исходный номер до успешной проверки полного переполнения.
for index, limit in ((2, 999), (1, 255), (0, 255)): # Patch имеет диапазон 0..999, minor и major занимают по одному октету 0..255.
if result[index] < limit: # Находит младший разряд, который ещё можно увеличить.
result[index] += 1 # Резервирует следующий номер попытки сборки, включая будущие неудачные компиляции.
return result # Старшие разряды сохраняются, переполненные младшие уже обнулены.
result[index] = 0 # Переносит единицу в следующий разряд после достижения предела текущего.
raise ValueError("Firmware version exhausted at 255.255.999") # Не допускает повторного использования номера после полного переполнения.
def prepare(repository: Path, templates: Path, config: Path, output: Path) -> dict:
settings = json.loads(config.read_text(encoding="utf-8"))
settings = json.loads(config.read_text(encoding="utf-8-sig"))
version = settings["version"]
if (len(version) != 3 or any(type(v) is not int for v in version)
or not all(0 <= v <= limit for v, limit in zip(version, (255, 255, 999)))):
raise ValueError("version must be [major 0..255, minor 0..255, patch 0..999]")
if not isinstance(version, list) or len(version) != 3 or any(type(value) is not int or not 0 <= value <= limit for value, limit in zip(version, (255, 255, 999))): # Проверяет JSON-массив трёх целых разрядов до любого изменения сохраняемого счётчика.
raise ValueError("version must be [major 0..255, minor 0..255, patch 0..999]") # Ошибочные типы, логические значения и выход за пределы контракта останавливают pre-build.
if type(settings.get("increment_version", False)) is not bool: # Отсутствующий флаг сохраняет ручной режим старых проектов; строки true/false не считаются настройкой.
raise ValueError("increment_version must be true or false") # Требует явное логическое значение JSON вместо неявного преобразования Python.
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: # Временный файл на том же диске позволяет атомарно заменить настройки без частичной записи.
temporary = Path(stream.name) # Запоминает только собственный файл, чтобы не затронуть пользовательские данные при очистке.
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: # Очистка должна работать и в 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"])
@@ -61,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))))
@@ -102,6 +139,86 @@ def verify(image: Path, manifest: Path) -> dict:
return metadata
def build_folder_name(metadata: dict) -> str:
"""Use the firmware commit and pre-build time rendered in UTC+3."""
source = metadata.get("source") or {}
dependency = metadata.get("templates") or {}
commit = source.get("commit")
if commit is not None:
if not isinstance(commit, str) or not re.fullmatch(r"[0-9a-fA-F]{40,64}", commit):
raise ValueError("invalid firmware commit in pre-build metadata")
identity = commit[:8].lower()
if source.get("dirty") or dependency.get("dirty"):
identity += "+"
else:
identity = metadata.get("build_id", "NOGIT000")
if not isinstance(identity, str) or not re.fullmatch(r"[0-9a-fA-F]{8}|[0-9a-fA-F]{7}\+|NOGIT000", identity):
raise ValueError("invalid build ID in pre-build metadata")
stamp_text = metadata.get("built_at_utc")
if not isinstance(stamp_text, str):
raise ValueError("missing pre-build UTC timestamp")
stamp = datetime.fromisoformat(stamp_text.replace("Z", "+00:00"))
if stamp.tzinfo is None:
raise ValueError("pre-build timestamp must include a timezone")
stamp = stamp.astimezone(timezone(timedelta(hours=3)))
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()
if export == folder or folder in export.parents:
raise ValueError("export directory must be outside the current build folder")
destination = export / folder.name
verify(folder / image_name, folder / (image_name + ".manifest.json"))
export.mkdir(parents=True, exist_ok=True)
if destination != folder:
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
def archive_build(image: Path, metadata_path: Path, output: Path = None,
extra_files=(), export: Path = None) -> dict:
"""Keep a byte-identical build snapshot, alongside the existing flat output."""
image, metadata_path = image.resolve(), metadata_path.resolve()
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
name = build_folder_name(metadata)
output = (output if output is not None else image.parent / "firmware").resolve()
output.mkdir(parents=True, exist_ok=True)
destination = output / name
# Seal and verify staged copies before publishing files in the build folder.
with tempfile.TemporaryDirectory(prefix=".archive-", dir=output) as temporary:
stage = Path(temporary)
shutil.copy2(image, stage / image.name)
for file in extra_files:
file = Path(file)
shutil.copy2(file, stage / file.name)
shutil.copy2(metadata_path, stage / "firmware_identity.json")
seal(stage / image.name, stage / "firmware_identity.json")
verify(stage / image.name, stage / (image.name + ".manifest.json"))
destination.mkdir(parents=True, exist_ok=True)
for file in stage.iterdir():
file.replace(destination / file.name)
print(f"[firmware-build] Folder: {destination}")
result = {"directory": str(destination), "file": image.name}
if export is not None:
result["exported_directory"] = str(export_build(destination, export, image.name))
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
@@ -114,9 +231,14 @@ def main():
p = sub.add_parser("verify")
p.add_argument("--image", type=Path, required=True)
p.add_argument("--manifest", type=Path, required=True)
p = sub.add_parser("archive")
p.add_argument("--image", type=Path, required=True)
p.add_argument("--metadata", dest="metadata_path", type=Path, required=True)
p.add_argument("--output", type=Path)
p.add_argument("--export", type=Path)
args = vars(parser.parse_args())
command = args.pop("command")
result = {"prepare": prepare, "seal": seal, "verify": verify}[command](**args)
result = {"prepare": prepare, "seal": seal, "verify": verify, "archive": archive_build}[command](**args)
print(json.dumps(result, ensure_ascii=False))
+2
View File
@@ -1,5 +1,7 @@
# GAS Logger — экспериментальная версия 1
> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми.
Непрерывный регистратор с двумя кольцевыми банками, неизменяемым снимком и
общей картой GAS / ProtoCAN GAS / Modbus FC03/FC06. Весь код и проектные/MCU
адаптеры находятся в `templates`; приложения подключают ревизию сабмодулем.
+21
View File
@@ -0,0 +1,21 @@
cmake_minimum_required(VERSION 3.13) # Задаёт минимальную версию, совместимую с соседними C-библиотеками templates.
project(global_time C) # Создаёт независимую цель для программных глобальных часов.
add_library(global_time STATIC src/global_time.c) # Собирает переносимое ядро без обязательных MCU заголовков.
target_include_directories(global_time PUBLIC include) # Экспортирует потребителю только публичный контракт часов.
target_compile_features(global_time PUBLIC c_std_99) # Требует C99 для точных целочисленных типов и uint64_t меток.
option(GLOBAL_TIME_BUILD_F28335 "Собирать аппаратный порт TI F28335" OFF) # Позволяет MCU проекту явно включить адаптер со своими TI include-путями.
if(GLOBAL_TIME_BUILD_F28335) # Не навязывает host-приложению аппаратные регистры TI.
target_sources(global_time PRIVATE ports/tms320f28335/global_time_f28335.c) # Добавляет eCAN и необязательный CPU Timer2 порт к той же библиотеке.
target_include_directories(global_time PUBLIC ports/tms320f28335) # Делает аппаратные hooks доступными прошивке потребителя.
endif() # Завершает условное подключение аппаратного адаптера.
option(GLOBAL_TIME_BUILD_TESTS "Собирать host-проверки глобального времени" ON) # Разрешает выключить host-исполняемые файлы в MCU сборке.
if(GLOBAL_TIME_BUILD_TESTS AND NOT GLOBAL_TIME_BUILD_F28335) # Не подмешивает host-модель регистров к настоящему TI порту.
enable_testing() # Регистрирует проверяемые сценарии в CTest.
add_executable(test_global_time tests/test_global_time.c) # Собирает календарные, CAN и монотонные сценарии.
target_link_libraries(test_global_time PRIVATE global_time) # Использует то же ядро, которое подключает прошивка.
add_test(NAME global_time COMMAND test_global_time) # Делает host-проверку доступной через ctest.
add_executable(test_global_time_f28335 tests/test_f28335.c ports/tms320f28335/global_time_f28335.c) # Собирает настоящий аппаратный адаптер с минимальной моделью регистров.
target_include_directories(test_global_time_f28335 PRIVATE tests/f28335_stubs ports/tms320f28335) # Ограничивает подмену заголовков только этой host-целью.
target_link_libraries(test_global_time_f28335 PRIVATE global_time) # Связывает аппаратные hooks с реальным ядром часов.
add_test(NAME global_time_f28335 COMMAND test_global_time_f28335) # Регистрирует eCAN/Timer2 проверку отдельно от календарной.
endif() # Завершает host-проверки без влияния на MCU исходники.
+167
View File
@@ -0,0 +1,167 @@
# Глобальное время
Программные часы с тактом 1 мс, синхронизацией даты и времени через ProtoCAN
и атомарным получением метки для логгера. Ядро C99 не требует аппаратного RTC,
операционной системы или динамической памяти. Порт TMS320F28335 включает
eCAN-A и необязательный CPU Timer2. Все текстовые файлы — UTF-8.
## Обмен при запуске
1. Приложение инициализирует часы до разрешения IRQ и запускает источник 1 мс.
2. После завершения инициализации **всей платы** вызывает `gt_ready()` /
`gt_f28335_ready()`.
3. Из main библиотека отправляет `DISCRETE/FLAGS`, `MsgBody=0x3000`,
`Route=1`, адрес самой платы, `DLC=1`, `DATA=01`.
4. Верхний уровень распознаёт пакет через `gt_parse_ready()`, берёт свою
текущую дату и посылает **один broadcast всем платам**, используя `gt_make_rtc()`.
5. Каждый узел принимает штатный `RTCSETUP`, сравнивает время со своим,
сохраняет величину коррекции и немедленно обновляет календарные часы.
При отсутствии ответа узел повторяет готовность не чаще раза в секунду.
`RTCSETUP` полностью сохраняет действующий формат ProtoCAN.
Универсальное сообщение готовности здесь определено как новый профиль
`DISCRETE/FLAGS`; ранее такого общего пакета в базовом протоколе не было.
Подробный [контракт обмена](../../doc/setcan/protocan/GLOBAL_TIME.md).
Код верхнего уровня для автоматической отправки времени подключается в его
приложении; библиотека предоставляет кодировщик и распознавание события старта.
## Метка логгера
`gt_get()` одним атомарным чтением возвращает:
| Поле | Значение |
|---|---|
| `seconds`, `millisecond` | секунды от 2000-01-01 и доля 0..999 мс |
| `synchronized` | 0 до первого корректного RTCSETUP, 1 после |
| `uptime_ms` | время работы modulo 2^32, независимое от переводов часов |
| `sync_uptime_ms` | момент приёма последнего опорного пакета |
| `generation` | номер принятой коррекции |
| `correction_ms` | последнее смещение, знаковое 64-битное значение в мс |
| `weekday` | фаза недели 0..6, продвигаемая при смене суток |
`gt_timestamp_ms()` формирует 64-битную метку от 2000 года.
`gt_calendar()` преобразует снимок в календарь и возвращает 0, пока дата неизвестна.
Для записей, которые должны различать переводы часов, сохраняйте также
`generation`, `synchronized` и/или `uptime_ms`. Календарное время может идти назад
после коррекции. Тайм-ауты следует считать по разности `uptime_ms`.
Без новых broadcast часы продолжают ход по локальному генератору;
возраст синхронизации равен `(uint32_t)(uptime_ms - sync_uptime_ms)`.
```c
#include "global_time_f28335.h" /* Подключает общую временную базу F28335. */
gt_stamp stamp; /* Содержит согласованную дату, миллисекунды и качество времени. */
gt_get(gt_f28335_clock(), &stamp); /* Допускается из main и ISR, включая ISR логгера. */
uint64_t time_ms = gt_timestamp_ms(&stamp); /* Перед сохранением календарной метки проверяется stamp.synchronized. */
```
Это фрагмент для функции потребителя. Формат хранения существующего логгера
библиотека не меняет. В частности, старое 32-битное поле `time_ms` в GAS logger
и v4 УМП не вмещает календарные миллисекунды: потребителю нужен собственный
формат абсолютной метки либо отдельная временная база и номер поколения.
## Подключение TMS320F28335
Добавьте в сборку `src/global_time.c` и
`ports/tms320f28335/global_time_f28335.c`. Include-пути: `include`,
`ports/tms320f28335`, штатные TI `DSP2833x_headers/include`,
`DSP2833x_common/include` и include компилятора.
Ресурсы порта:
| Ресурс | Назначение |
|---|---|
| eCAN-A MBOX7 | RX точного RTCSETUP, любой приоритет, IRQ line0 |
| eCAN-A MBOX8 | TX готовности, ACK проверяется в main |
| CPU Timer2 / INT14 | необязательный собственный такт 1 мс |
Mailbox 0..6, скорость CAN, GPIO и глобальные маршруты PIE/CANGIM настраивает
приложение. `DBO=0` обязателен; порт отвергает другую раскладку октетов.
MBOX7 использует CANOPC: кадр защищён от аппаратной перезаписи до копирования ISR.
В программной очереди хранится последний broadcast, поскольку более новый
опорный пакет заменяет предыдущий.
При запрещённых IRQ, **после** `InitECan` и `InitPieVectTable`:
```c
int ok = gt_f28335_init_periph(5U); /* PERIPH5=Type1/ID5, то есть УМП1; номер берётся из конфигурации платы. */
if (ok) ok = gt_f28335_timer2_start(150000000UL); /* Частота CPU задаётся проектом в Гц; пример соответствует 150 МГц. */
```
`gt_f28335_init()` позволяет передать Type и ID непосредственно.
`init_periph()` поддерживает физические PERIPH 0..31: Type1/2 и ID0..15;
5 — УМП1, 6 — УМП2, 8 — задатчик. Порт инициализируется до подключения hooks.
Повторный init разрешён только при остановленном собственном Timer2,
отсутствующем TX и запрещённых глобальных IRQ.
Если Timer2 занят RTOS или приложением, **не вызывайте** `timer2_start()`;
в существующем ISR с периодом ровно 1 мс вызывайте `gt_f28335_tick_1ms()`.
Используйте один источник такта. Отдельный Timer2 продолжает отсчёт и при
медленном main, но длительная блокировка IRQ на несколько миллисекунд приводит
к потере тиков: аппаратный флаг прерывания не считает пропущенные периоды.
В существующем `ECAN0INTA` ISR перед очисткой общих CAN-флагов и PIEACK:
```c
gt_f28335_rx_isr(); /* Копирует только MBOX7 и запоминает uptime при приёме, календарь проверяется позднее. */
```
Приложение должно разрешить соответствующий CAN IRQ: `CANGIM.I0EN`,
PIE group9 `INTx5`, CPU `M_INT9`; оставить свой обработчик остальных mailbox
и штатные подтверждения CANGIF/PIEACK. Порт не заменяет общий CAN ISR.
Глобальный `EINT` выполняет приложение после полной настройки.
После прикладной инициализации и в каждом проходе главного цикла соответственно:
```c
gt_f28335_ready(); /* Выполняется один раз после загрузки параметров и запуска прикладных подсистем. */
gt_f28335_service(); /* Выполняется регулярно в main; обработка календаря и CAN не ждёт аппаратного ACK. */
```
Все атомарные чтения и обновления используют TI
`__disable_interrupts()` / `__restore_interrupts()`, сохраняя прежний INTM.
Транспорт ждёт подтверждение CANTA асинхронно и не перезаписывает занятый MBOX8.
При отсутствии других CAN-узлов передача остаётся аппаратному контроллеру
до восстановления шины. CAN ACK не подтверждает обработку пакета мастером;
приход корректного RTCSETUP прекращает повторные объявления готовности.
## Точность и календарь
Разрешение локальных часов — 1 мс. Действующий RTCSETUP передаёт только целые
секунды, поэтому сам формат **не гарантирует синхронизацию плат с точностью 1 мс**.
Мастер должен посылать актуальное значение около секундной границы.
Арбитраж CAN и задержка до RX ISR остаются погрешностью; задержка после RX ISR
до обработки в main компенсируется по монотонному счётчику. Для точности между
платами до 1 мс понадобится согласованное расширение проводного протокола.
Одна шкала времени выбирается верхним уровнем (рекомендуется UTC); кадр не
содержит часовой пояс. Входные годы 2000..2099, дата проверяется по григорианскому
календарю. Формат дня недели 0..6 соответствует проверке существующего
STM32-порта; профиль обозначает 0 как воскресенье. Для совместимости переданная
фаза недели сохраняется без дополнительной проверки соответствия дате.
Некоторые старые клиенты отображают неделю как 1..7; перед отправкой им нужен
перевод воскресенья 7 → 0.
Секундная шкала uint32_t от 2000 года переполнится в феврале 2136 года.
Период между захватом RX uptime и `gt_receive()` должен быть меньше 2^32 мс.
## Проверки
Из корня templates:
```text
python c/global-time/tests/run_tests.py
```
`--cc` задаёт GCC/Clang. Для отдельной TI C28x проверки передайте
`--ti-cgt` (каталог компилятора), `--ti-headers` (DSP2833x_headers/include)
и при необходимости `--ti-common` (DSP2833x_common/include).
Объекты и EXE создаются во временном каталоге. CMake также предоставляет
`global_time`, два CTest и опции `GLOBAL_TIME_BUILD_F28335` /
`GLOBAL_TIME_BUILD_TESTS`; MCU проект задаёт свои TI include-пути.
Проверяются ошибки даты и кадра, високосный февраль, смена года, коррекции
в обе стороны, компенсация очереди через переполнение uptime, ранний broadcast,
повторы готовности, сохранение IRQ, изоляция eCAN mailbox, аппаратный ACK,
значения периода Timer2 и его реальный ISR через назначенный вектор.
TI-проверка компилирует исходники с large/COFF ABI и настоящими заголовками
проекта 28335_periph. Полная линковка конкретной прошивки и проверка на плате
в этот набор не входят.
+65
View File
@@ -0,0 +1,65 @@
#ifndef GLOBAL_TIME_H /* Защищает единый контракт часов от повторного включения. */
#define GLOBAL_TIME_H /* Отмечает подключённый интерфейс программных глобальных часов. */
#include <stdint.h> /* Фиксирует разрядность счётчиков и поддерживает 16-битный char TI C28x. */
#ifdef __cplusplus /* Сохраняет совместимость C-библиотеки с приложениями C++. */
extern "C" { /* Экспортирует функции без изменения имён компилятором C++. */
#endif /* Завершает условное объявление C ABI. */
#define GT_RTC_ID 0x00000030UL /* ProtoCAN: Route=0, Type/ID=0, BROADCAST=0, RTCSETUP=3 в Body[15:4]. */
#define GT_READY_BODY 0x3000U /* Контракт GLOBAL_TIME.md: DISCRETE/FLAGS=3, страница общих флагов=0. */
#define GT_READY_RETRY_MS 1000UL /* Повторяет сообщение готовности раз в секунду до первой синхронизации. */
typedef struct gt_frame { /* Описывает classic CAN независимо от регистров и размера char процессора. */
uint32_t id; /* Хранит только 29 бит идентификатора, без служебных битов драйвера. */
uint16_t extended; /* Единица означает IDE=1; стандартные кадры часы игнорируют. */
uint16_t remote; /* Единица означает RTR; удалённый запрос не содержит время. */
uint16_t dlc; /* Считает октеты на линии, а не sizeof элементов массива C28x. */
uint16_t data[8]; /* Каждый элемент содержит один октет 0..255 даже при CHAR_BIT=16. */
} gt_frame; /* Завершает переносимый контейнер кадра. */
typedef struct gt_datetime { /* Представляет календарь без зависимости от time_t и часового пояса ОС. */
uint16_t year; /* Полный год; входной ProtoCAN поддерживает 2000..2099. */
uint16_t month; /* Номер месяца 1..12, без BCD-кодирования. */
uint16_t day; /* Число месяца с проверкой високосного февраля. */
uint16_t weekday; /* День недели 0..6; профиль использует 0=воскресенье, 1=понедельник. */
uint16_t hour; /* Часы 0..23 в 24-часовом формате. */
uint16_t minute; /* Минуты 0..59; leap seconds протокол не передаёт. */
uint16_t second; /* Секунды 0..59 согласно существующему RTCSETUP. */
uint16_t millisecond; /* Доля секунды 0..999; RTCSETUP задаёт нулевую долю. */
} gt_datetime; /* Завершает публичный календарный тип. */
typedef struct gt_stamp { /* Даёт логгеру согласованную метку и её качество одним чтением. */
uint32_t seconds; /* Секунды от 2000-01-01 00:00:00 в шкале времени мастера. */
uint16_t millisecond; /* Миллисекундная доля текущей секунды. */
uint16_t synchronized; /* Ноль до первого корректного RTCSETUP, единица после него. */
uint32_t uptime_ms; /* Независимый счётчик работы modulo 2^32; коррекции его не меняют. */
uint32_t sync_uptime_ms; /* Момент приёма последнего RTCSETUP для оценки давности синхронизации. */
uint32_t generation; /* Число коррекций; логгер отличает участки до и после перевода часов. */
int64_t correction_ms; /* Последнее смещение новых часов относительно старых, в миллисекундах. */
uint16_t weekday; /* Принятый день недели, продвигаемый при каждом переходе через полночь. */
} gt_stamp; /* Завершает атомарно читаемый снимок. */
typedef struct gt_port { /* Передаёт ядру короткие критические секции и неблокирующую отправку CAN. */
void *user; /* Контекст платформы, передаваемый каждому callback без преобразования. */
uint16_t (*enter)(void *user); /* Запрещает IRQ и возвращает прежнее состояние для вложенных вызовов. */
void (*leave)(void *user, uint16_t state); /* Восстанавливает прежнее состояние IRQ, включая вызовы из ISR. */
int (*send)(void *user, const gt_frame *frame); /* Копирует кадр до возврата; 1=принят, 0=занято/ошибка. */
} gt_port; /* Завершает платформенный контракт; heap и аппаратный RTC не нужны. */
typedef struct gt_clock { /* Хранит состояние одного узла; инициализация обязательна до разрешения ISR. */
gt_port port; /* Содержит проверенные callbacks и контекст платформы. */
volatile gt_stamp stamp; /* Меняется только при удержании блокировки IRQ. */
uint16_t device_type; /* Три адресных бита ProtoCAN, диапазон 0..7. */
uint16_t device_id; /* Четыре адресных бита экземпляра, диапазон 0..15. */
uint16_t ready; /* Устанавливается приложением после завершения всей инициализации платы. */
uint16_t announced; /* Помнит принятую транспортом отправку готовности, даже если часы уже синхронизированы. */
uint32_t sent_uptime_ms; /* Ограничивает частоту повторов готовности монотонной шкалой. */
} gt_clock; /* Состояние допускает статическое выделение памяти на микроконтроллере. */
int gt_init(gt_clock *clock, const gt_port *port, uint16_t device_type, uint16_t device_id); /* Проверяет адрес/callbacks и явно очищает RAM, включая TI COFF. */
void gt_tick_1ms(gt_clock *clock); /* Вызывается ровно один раз за аппаратную миллисекунду и выполняет только короткую арифметику. */
void gt_get(gt_clock *clock, gt_stamp *out); /* Атомарно копирует всю метку; допустим вызов из main, логгера и ISR. */
void gt_ready(gt_clock *clock); /* Идемпотентно разрешает сообщение «плата инициализирована»; CAN отправляется из poll. */
void gt_poll(gt_clock *clock); /* Вызывается единственным потребителем в main; не ждёт освобождения CAN. */
int gt_receive(gt_clock *clock, const gt_frame *frame, uint32_t received_uptime_ms); /* Проверяет RTCSETUP и компенсирует задержку от приёма до обработки, менее 2^32 мс. */
int gt_make_rtc(const gt_datetime *date, gt_frame *out); /* Даёт верхнему уровню кодировщик штатного 7-октетного broadcast. */
int gt_parse_ready(const gt_frame *frame, uint16_t *device_type, uint16_t *device_id); /* Позволяет мастеру распознать готовность и выбрать ответ RTCSETUP. */
int gt_calendar(const gt_stamp *stamp, gt_datetime *out); /* Преобразует согласованный снимок в календарь; до синхронизации возвращает 0. */
uint64_t gt_timestamp_ms(const gt_stamp *stamp); /* Возвращает широкую метку от 2000 года без переполнения через 49,7 суток. */
#ifdef __cplusplus /* Закрывает C ABI только для компилятора C++. */
} /* Завершает блок функций с C-связыванием. */
#endif /* Завершает проверку языка потребителя. */
#endif /* Завершает защиту публичного заголовка. */
@@ -0,0 +1,168 @@
#include "DSP2833x_Device.h" /* Использует штатные описания 32-битных регистров eCAN, Timer2 и векторов TI. */
#include "global_time_f28335.h" /* Подключает переносимые часы и контракт интеграции порта. */
#include <string.h> /* Явно очищает отложенный RX до первого прохода main, включая TI COFF. */
#define GT_RX 0x00000080UL /* Бит 7 регистров eCAN соответствует выделенному приёмному MBOX7. */
#define GT_TX 0x00000100UL /* Бит 8 регистров eCAN соответствует отдельному передающему MBOX8. */
static gt_clock board_clock; /* Один узел F28335 использует одну глобальную временную шкалу. */
static gt_frame pending_frame; /* Последний RX хранится до обработки в main; новый broadcast заменяет устаревший. */
static volatile uint16_t pending_rx; /* Публикует полностью скопированный RX и его монотонную привязку. */
static uint32_t received_at; /* Запоминает миллисекундный uptime в момент обработки CAN ISR. */
static uint16_t tx_busy, timer_owned; /* Явно сбрасывается init: TI COFF не гарантирует нули в .ebss. */
static uint16_t initialized = 0U; /* Запись в .cinit гарантирует безопасный отказ ранних ISR hooks до init даже при TI COFF. */
static uint16_t enter(void *user) /* Реализует вложенную критическую секцию TI C28x. */
{ /* Не разрешает прерывания при выходе из уже выполняющегося ISR. */
(void)user; /* Порт использует один статический экземпляр, поэтому контекст не требуется. */
return __disable_interrupts(); /* Возвращает предыдущее состояние ST1, необходимое для восстановления INTM. */
} /* Завершает платформенный вход в критическую секцию. */
static void leave(void *user, uint16_t state) /* Восстанавливает состояние IRQ до входа в библиотеку. */
{ /* Сохраняет корректность вызовов и из main, и из ISR. */
(void)user; /* Отдельный аппаратный экземпляр выбирается статическими регистрами eCAN-A. */
__restore_interrupts(state); /* Восстанавливает исходный INTM вместо безусловного разрешения IRQ. */
} /* Завершает платформенный выход из критической секции. */
static int send(void *user, const gt_frame *frame) /* Копирует пакет готовности в выделенный аппаратный mailbox без ожидания. */
{ /* Транспорт не перезаписывает кадр, пока контроллер не подтвердит передачу. */
uint16_t state = enter(0); /* Защищает RMW общих регистров CANME от чужих CAN ISR, сохраняя прежний INTM. */
(void)user; /* Контекст платформы не нужен для фиксированного eCAN-A MBOX8. */
if (tx_busy || (ECanaRegs.CANTRS.all & GT_TX)) { /* При занятости сохраняет исходный пакет и предлагает повтор из main. */
leave(0, state); /* Восстанавливает исходные IRQ и на раннем выходе занятого транспорта. */
return 0; /* Не теряет готовность и не ждёт окончания аппаратной передачи. */
} /* Продолжает только при владении свободным MBOX8. */
EALLOW; /* Открывает запись защищённой конфигурации mailbox контроллера. */
ECanaRegs.CANME.all &= ~GT_TX; /* Отключает только MBOX8 перед изменением ID и payload. */
ECanaMboxes.MBOX8.MSGID.all = 0x80000000UL | frame->id; /* IDE в бите 31 eCAN разрешает расширенный ProtoCAN ID. */
ECanaMboxes.MBOX8.MSGCTRL.all = 1UL; /* DLC=1 и RTR=0 соответствуют единственному октету готовности. */
ECanaMboxes.MBOX8.MDL.all = frame->data[0]; /* Первый проводной октет располагается в младших восьми битах MDL при DBO=0. */
ECanaMboxes.MBOX8.MDH.all = 0UL; /* Убирает остатки прежнего payload, хотя DLC передаёт только один октет. */
ECanaRegs.CANTA.all = GT_TX; /* Сбрасывает только старый ACK MBOX8 записью единицы. */
ECanaRegs.CANAA.all = GT_TX; /* Сбрасывает только старое подтверждение отмены MBOX8. */
ECanaRegs.CANME.all |= GT_TX; /* Возвращает MBOX8 в работу, сохраняя чужие mailbox. */
ECanaRegs.CANTRS.all = GT_TX; /* Запрашивает передачу без блокирующего ожидания ACK шины. */
EDIS; /* Закрывает запись защищённых регистров после полной публикации кадра. */
tx_busy = 1U; /* Отмечает владение кадром до CANTA или CANAA, даже при отсутствии других узлов на шине. */
leave(0, state); /* Освобождает короткую аппаратную секцию сразу после публикации запроса передачи. */
return 1; /* Подтверждает приём транспортом, но не отдельное прикладное подтверждение мастера. */
} /* Завершает неблокирующий callback передачи. */
int gt_f28335_init(uint16_t device_type, uint16_t device_id) /* Настраивает часы и только два выделенных mailbox при запрещённых IRQ. */
{ /* Приложение заранее задаёт скорость eCAN, GPIO, DBO=0 и общие векторы прерываний. */
gt_port port; /* Локальная таблица callbacks копируется ядром в постоянное состояние. */
if (device_type > 7U || device_id > 15U || (ECanaRegs.CANTRS.all & GT_TX) || ECanaRegs.CANMC.bit.DBO) return 0; /* Проверяет адрес и порядок октетов; активную TX-передачу не прерывает повторным init. */
if (initialized && timer_owned) return 0; /* Требует остановить захваченный Timer2 перед повторным init, сохраняя его владение при отказе. */
port.user = 0; /* Единственный экземпляр порта не требует внешнего контекста. */
port.enter = enter; /* Подключает защиту снимка штатным intrinsic TI. */
port.leave = leave; /* Подключает восстановление предыдущей маски прерываний. */
port.send = send; /* Подключает отправку в MBOX8 без ожидания свободной шины. */
if (!gt_init(&board_clock, &port, device_type, device_id)) return 0; /* Сбрасывает RAM до открытия источников IRQ. */
pending_rx = tx_busy = timer_owned = 0U; /* Явно очищает программные флаги после холодного и тёплого старта. */
memset(&pending_frame, 0, sizeof(pending_frame)); /* Не читает неопределённую RAM при первом service без полученного пакета. */
received_at = 0UL; /* Начальная привязка не читается до получения первого кадра. */
EALLOW; /* Открывает изменение аппаратной конфигурации eCAN-A. */
ECanaRegs.CANME.all &= ~(GT_RX | GT_TX); /* Временно отключает только выделенные mailbox 7/8. */
ECanaMboxes.MBOX7.MSGID.all = 0xC0000000UL | GT_RTC_ID; /* IDE=1 и AME=1 включают расширенный ID и локальную маску точного RTCSETUP. */
ECanaLAMRegs.LAM7.all = 0x10000000UL; /* LAM игнорирует только бит Priority[28]; LAMI=0 сохраняет проверку IDE. */
ECanaMboxes.MBOX7.MSGCTRL.all = 7UL; /* Задаёт ожидаемые семь октетов; фактический DLC дополнительно проверяется ядром. */
ECanaMboxes.MBOX8.MSGID.all = 0x80000000UL; /* Подготавливает расширенный TX без AME; адрес будет установлен callback отправки. */
ECanaMboxes.MBOX8.MSGCTRL.all = 1UL; /* Пакет готовности занимает один октет CAN payload. */
ECanaRegs.CANMD.all = (ECanaRegs.CANMD.all | GT_RX) & ~GT_TX; /* MBOX7 становится RX, MBOX8 — TX, направления остальных mailbox сохраняются. */
ECanaRegs.CANOPC.all |= GT_RX; /* Запрещает перезапись RX до чтения ISR; при потере используется следующий периодический broadcast. */
ECanaRegs.CANMIL.all &= ~GT_RX; /* Направляет MBOX7 в общую линию ECAN0INTA приложения. */
ECanaRegs.CANMIM.all = (ECanaRegs.CANMIM.all | GT_RX) & ~GT_TX; /* Разрешает IRQ только на RX, а завершение TX проверяет main. */
ECanaRegs.CANRMP.all = GT_RX; /* Удаляет старый pending только MBOX7 записью единицы. */
ECanaRegs.CANRML.all = GT_RX; /* Очищает старую потерю приёма только выделенного mailbox. */
ECanaRegs.CANTA.all = GT_TX; /* Удаляет старый ACK выделенного передатчика, не затрагивая CAN Boot. */
ECanaRegs.CANAA.all = GT_TX; /* Удаляет старый флаг отмены перед настройкой нового обмена. */
ECanaRegs.CANME.all |= GT_RX | GT_TX; /* Включает приём синхронизации и передачу готовности. */
EDIS; /* Закрывает защищённую конфигурацию контроллера. */
initialized = 1U; /* Публикует готовность самого порта после сброса всех RAM- и аппаратных флагов. */
return 1; /* Приложение может запускать такт и разрешать общие CAN-прерывания. */
} /* Завершает настройку без сброса скорости, GPIO и других CAN-сервисов. */
int gt_f28335_init_periph(uint16_t periph) /* Привязывает часы к физическому адресу периферийной платы. */
{ /* Использует тот же реестр, что существующий порт CAN Boot F28335. */
if (periph >= 32U) return 0; /* Адреса PERIPH определены только для 32 устройств, без циклического обрезания. */
return gt_f28335_init((uint16_t)(1U + periph / 16U), periph % 16U); /* Номера 0..15 используют Type=1, 16..31 — Type=2, младшие четыре бита задают ID. */
} /* Завершает преобразование физического адреса. */
gt_clock *gt_f28335_clock(void) /* Предоставляет логгеру общую временную базу платы. */
{ /* Действует после успешной инициализации порта. */
return &board_clock; /* Один и тот же объект обслуживают CAN, таймер и все потребители времени. */
} /* Завершает доступ к экземпляру переносимого ядра. */
void gt_f28335_ready(void) /* Публикует готовность прикладного оборудования после загрузки параметров. */
{ /* Не объявляет успех при отказе конфигурации порта. */
if (initialized) gt_ready(&board_clock); /* Первый пакет будет послан из main даже при уже полученном времени. */
} /* Завершает hook завершения инициализации платы. */
void gt_f28335_tick_1ms(void) /* Позволяет использовать существующий периодический ISR вместо Timer2. */
{ /* Приложение выбирает один источник миллисекунд, исключая двойной отсчёт. */
if (initialized) gt_tick_1ms(&board_clock); /* Продвигает локальные и календарные часы только после явного init. */
} /* Завершает универсальный миллисекундный hook. */
void gt_f28335_rx_isr(void) /* Захватывает CAN-время и момент получения без календарной обработки в ISR. */
{ /* Вызывается существующим обработчиком ECAN0INTA до очистки CANGIF и PIEACK. */
uint32_t low, high, uptime; /* Однократные 32-битные чтения не рвут слова аппаратного mailbox. */
uint16_t i, state; /* Индекс восьми октетов и прежняя маска прерываний. */
if (!initialized || !(ECanaRegs.CANRMP.all & GT_RX)) return; /* Не читает MBOX7 по прерыванию другого CAN-сервиса. */
state = enter(0); /* Исключает вложенный таймер между чтением uptime и публикацией полученного кадра. */
uptime = board_clock.stamp.uptime_ms; /* Фиксирует момент RX в монотонных миллисекундах, независимых от синхронизации. */
pending_frame.id = ECanaMboxes.MBOX7.MSGID.all & 0x1FFFFFFFUL; /* Удаляет служебные IDE/AME биты регистра из 29-битного ID. */
pending_frame.extended = (uint16_t)(ECanaMboxes.MBOX7.MSGID.all >> 31U); /* Ядро дополнительно проверит IDE даже при аппаратной фильтрации. */
pending_frame.remote = ECanaMboxes.MBOX7.MSGCTRL.bit.RTR; /* Сохраняет RTR, чтобы удалённый запрос никогда не устанавливал часы. */
pending_frame.dlc = ECanaMboxes.MBOX7.MSGCTRL.bit.DLC; /* Сохраняет фактическую длину, включая ошибочный или пустой payload. */
low = ECanaMboxes.MBOX7.MDL.all; /* Читает первые четыре октета одним 32-битным доступом eCAN. */
high = ECanaMboxes.MBOX7.MDH.all; /* Читает оставшиеся четыре октета без указателей на байты C28x. */
for (i = 0U; i < 8U; ++i) pending_frame.data[i] = (uint16_t)(((i < 4U ? low : high) >> (8U * (i % 4U))) & 255UL); /* Извлекает каждый проводной октет при DBO=0 в отдельное 16-битное слово. */
received_at = uptime; /* Публикует временную привязку того же аппаратного кадра. */
pending_rx = 1U; /* Отмечает полностью готовый снимок; main читает его под той же блокировкой. */
ECanaRegs.CANRMP.all = GT_RX; /* Освобождает MBOX7 только после полного копирования защищённого CANOPC кадра. */
ECanaRegs.CANRML.all = GT_RX; /* Сбрасывает индикатор пропущенного кадра, сохраняя уже скопированное опорное время. */
leave(0, state); /* Возвращает исходный INTM обработчика CAN. */
} /* Завершает короткий ISR без отправки, ожидания CAN и расчёта календаря. */
void gt_f28335_service(void) /* Обрабатывает часы и готовность из единственного основного цикла. */
{ /* Отложенная календарная работа не увеличивает длительность CAN ISR. */
gt_frame frame; /* Локальная копия допускает замену следующего pending кадра новым CAN ISR. */
uint32_t uptime; /* Сохраняет привязку прочитанного кадра до выхода из критической секции. */
uint16_t state, have_rx; /* Разделяет короткий захват очереди и длительную проверку календаря. */
if (!initialized) return; /* Не обращается к неинициализированным callbacks после отказа init. */
state = enter(0); /* Блокирует ISR только на время копирования одного небольшого кадра. */
have_rx = pending_rx; /* Запоминает наличие кадра перед освобождением слота. */
frame = pending_frame; /* Копирует payload и заголовок согласованно с моментом RX. */
uptime = received_at; /* Забирает момент получения того же кадра. */
pending_rx = 0U; /* Разрешает приём следующего broadcast, пока main рассчитывает дату текущего. */
leave(0, state); /* Возобновляет IRQ до календарных циклов. */
if (have_rx) (void)gt_receive(&board_clock, &frame, uptime); /* Проверяет время и добавляет тики, прошедшие с RX ISR. */
if (tx_busy && ((ECanaRegs.CANTA.all | ECanaRegs.CANAA.all) & GT_TX)) { /* Освобождает TX только после подтверждения передачи либо аппаратной отмены. */
ECanaRegs.CANTA.all = GT_TX; /* Сбрасывает только ACK собственного mailbox. */
ECanaRegs.CANAA.all = GT_TX; /* Сбрасывает только флаг отмены собственного mailbox. */
tx_busy = 0U; /* Следующий gt_poll сможет повторить готовность, если время ещё не получено. */
} /* При отсутствии ACK запрос остаётся аппаратному CAN для автоматической передачи после восстановления шины. */
gt_poll(&board_clock); /* Объявляет готовность и ограничивает повторы одной секундой локального времени. */
} /* Завершает неблокирующий проход обслуживания. */
__interrupt void gt_f28335_timer2_isr(void) /* Обслуживает прямой CPU INT14 без использования группы PIE. */
{ /* Этот вектор принадлежит библиотеке только после явного timer2_start. */
CpuTimer2Regs.TCR.all = 0xC000U; /* TIF[15]=1 очищает флаг, TIE[14]=1 сохраняет IRQ, TSS=0 продолжает счёт. */
gt_f28335_tick_1ms(); /* Один период Timer2 соответствует одной миллисекунде программных часов. */
} /* Для прямого INT14 подтверждение PIEACK не требуется. */
int gt_f28335_timer2_start(uint32_t cpu_hz) /* Опционально запускает выделенный таймер с периодом ровно 1 мс. */
{ /* Частота поступает от проекта; порт не предполагает жёстко заданные 150 МГц. */
uint16_t state; /* Сохраняет прежнюю глобальную маску IRQ на время смены вектора. */
if (!initialized || cpu_hz < 1000UL || cpu_hz % 1000UL != 0UL) return 0; /* Отвергает период с дробным числом тактов вместо накопления скрытого дрейфа. */
state = enter(0); /* Исключает запуск INT14 со старым вектором и новым периодом. */
CpuTimer2Regs.TCR.all = 0x0010U; /* TSS[4]=1 останавливает счёт, TIE=0 временно запрещает IRQ Timer2. */
CpuTimer2Regs.PRD.all = cpu_hz / 1000UL - 1UL; /* Период включает значения PRD..0; при 150 МГц это 149999, то есть 150000 тактов за 1 мс. */
CpuTimer2Regs.TPR.all = 0U; /* Нулевой младший предделитель означает один такт CPU на шаг счётчика. */
CpuTimer2Regs.TPRH.all = 0U; /* Нулевой старший предделитель не добавляет деления частоты. */
EALLOW; /* Разрешает запись защищённой таблицы векторов прерываний TI. */
PieVectTable.TINT2 = &gt_f28335_timer2_isr; /* Подключает собственный обработчик прямого CPU Timer2/INT14. */
EDIS; /* Закрывает доступ к векторной таблице после установки обработчика. */
IFR &= (uint16_t)~M_INT14; /* Снимает старый pending CPU INT14 до запуска нового периодического такта. */
IER |= M_INT14; /* Разрешает только INT14, сохраняя маски остальных CPU-прерываний. */
timer_owned = 1U; /* Позволяет stop изменять Timer2 только после явного захвата этим портом. */
CpuTimer2Regs.TCR.all = 0xC020U; /* TIF=1 очищает флаг, TIE=1 разрешает IRQ, TRB=1 загружает PRD, TSS=0 запускает таймер. */
leave(0, state); /* Сохраняет выбор приложения, когда разрешить глобальные прерывания. */
return 1; /* Приложение должно исключить параллельный вызов tick из своего Timer1. */
} /* Завершает настройку автономного миллисекундного такта. */
void gt_f28335_timer2_stop(void) /* Освобождает аппаратный такт, ранее явно запущенный библиотекой. */
{ /* Не трогает чужой Timer2, если выбран hook существующего таймера. */
uint16_t state = enter(0); /* Не допускает INT14 между остановкой счётчика и снятием разрешения CPU. */
if (timer_owned) { /* Ограничивает изменения ресурсом, захваченным этим портом. */
CpuTimer2Regs.TCR.all = 0x8010U; /* TIF=1 очищает флаг, TSS=1 останавливает таймер, TIE=0 запрещает источник IRQ. */
IER &= (uint16_t)~M_INT14; /* Убирает только разрешение прямого Timer2 IRQ, сохраняя остальные CPU-сервисы. */
timer_owned = 0U; /* Фиксирует освобождение источника аппаратного такта. */
} /* При отсутствии владения никакие регистры таймера не меняются. */
leave(0, state); /* Возвращает прежнее состояние глобальной маски IRQ. */
} /* Завершает необязательную остановку аппаратных часов. */
@@ -0,0 +1,19 @@
#ifndef GLOBAL_TIME_F28335_H /* Защищает порт F28335 от повторного включения. */
#define GLOBAL_TIME_F28335_H /* Отмечает единственный экземпляр аппаратного порта. */
#include "global_time.h" /* Экспортирует тот же снимок метки и календарный API, что переносимое ядро. */
#ifdef __cplusplus /* Сохраняет возможность подключения из приложения C++. */
extern "C" { /* Фиксирует C ABI аппаратных точек входа. */
#endif /* Завершает условное открытие C ABI. */
int gt_f28335_init(uint16_t device_type, uint16_t device_id); /* После InitECan и InitPieVectTable, до IRQ: занимает только eCAN-A MBOX7 RX и MBOX8 TX. */
int gt_f28335_init_periph(uint16_t periph); /* Преобразует PERIPH 0..31 в Type=1/2, ID=0..15 по реестру ProtoCAN. */
gt_clock *gt_f28335_clock(void); /* Даёт логгеру ядро для gt_get; указатель действителен после успешного init. */
void gt_f28335_ready(void); /* Вызывается после успешной инициализации всей платы, затем отправка происходит из service. */
void gt_f28335_tick_1ms(void); /* Подключается к существующему ISR 1 мс, если отдельный Timer2 не используется. */
void gt_f28335_rx_isr(void); /* Вызывается общим ECAN0INTA ISR до очистки общих флагов; не выполняет календарных циклов. */
void gt_f28335_service(void); /* Вызывается в main: применяет последний RX, обслуживает ACK и повторы готовности без ожиданий. */
int gt_f28335_timer2_start(uint32_t cpu_hz); /* Необязательный такт: занимает CPU Timer2/INT14, принимает частоту CPU в Гц, кратную 1000. */
void gt_f28335_timer2_stop(void); /* Останавливает только ранее запущенный портом Timer2, сохраняя остальные разрешения CPU IRQ. */
#ifdef __cplusplus /* Закрывает C ABI при использовании C++. */
} /* Завершает блок аппаратных функций с C-связыванием. */
#endif /* Завершает языковую проверку. */
#endif /* Завершает защиту заголовка порта. */
+156
View File
@@ -0,0 +1,156 @@
#include "global_time.h" /* Подключает переносимые типы, API часов и фиксированные поля ProtoCAN. */
#include <string.h> /* Использует явный сброс состояния вместо предположений о старте TI COFF. */
static uint16_t month_days(uint16_t year, uint16_t month) /* Вычисляет длину уже проверенного месяца по григорианскому календарю. */
{ /* Ограничивает область таблицы календаря и условия високосного года. */
static const uint16_t days[12] = {31,28,31,30,31,30,31,31,30,31,30,31}; /* Число суток каждого месяца обычного года. */
return (uint16_t)(days[month - 1U] + (month == 2U && year % 4U == 0U && (year % 100U != 0U || year % 400U == 0U))); /* Добавляет 29 февраля, включая исключение для 2100 года. */
} /* Завершает календарный расчёт без обращения к платформе. */
static int date_seconds(const gt_datetime *date, uint32_t *seconds) /* Проверяет все входные поля до индексации календарных таблиц. */
{ /* Собирает секунды эпохи отдельно от критической секции часов. */
uint16_t year, month; /* Счётчики лет и месяцев нужны только при редкой синхронизации. */
uint32_t days = 0; /* Накопленные сутки от начала 2000 года; диапазон CAN не превышает 36525 суток. */
if (!date || date->year < 2000U || date->year > 2099U || date->month < 1U || date->month > 12U) return 0; /* Отвергает неподдерживаемую эпоху и неверный индекс месяца. */
if (date->day < 1U || date->day > month_days(date->year, date->month) || date->weekday > 6U) return 0; /* Принимает только существующую дату и формат дня недели действующего STM32 RTCSETUP. */
if (date->hour > 23U || date->minute > 59U || date->second > 59U || date->millisecond > 999U) return 0; /* Исключает перенос некорректных полей в соседние сутки. */
for (year = 2000U; year < date->year; ++year) days += 365UL + (month_days(year, 2U) == 29U); /* Учитывает каждый полный год относительно фиксированной эпохи. */
for (month = 1U; month < date->month; ++month) days += month_days(date->year, month); /* Прибавляет полные месяцы текущего года. */
days += date->day - 1U; /* Переводит календарное число 1..31 в число завершённых суток. */
*seconds = days * 86400UL + (uint32_t)date->hour * 3600UL + (uint32_t)date->minute * 60UL + date->second; /* Константы задают секунд в сутках, часе и минуте; UL исключает 16-битное умножение C28x. */
return 1; /* Подтверждает полностью проверенную календарную величину. */
} /* Завершает преобразование календаря в секундную шкалу. */
uint64_t gt_timestamp_ms(const gt_stamp *stamp) /* Позволяет логгеру сохранить абсолютное время без потери миллисекунд. */
{ /* Выполняет расчёт по уже снятому атомарному снимку. */
return (uint64_t)stamp->seconds * 1000ULL + stamp->millisecond; /* Расширяет до умножения, чтобы календарная метка не переполняла uint32_t. */
} /* Завершает получение широкой метки времени. */
int gt_init(gt_clock *clock, const gt_port *port, uint16_t device_type, uint16_t device_id) /* Создаёт часы до запуска прерываний устройства. */
{ /* Отказывается от конфигурации без реальной защиты общего состояния. */
if (!clock || !port || !port->enter || !port->leave || !port->send || device_type > 7U || device_id > 15U) return 0; /* Не обрезает ошибочный CAN-адрес до адреса другой платы. */
memset(clock, 0, sizeof(*clock)); /* Сбрасывает в том числе неинициализированную .ebss TI COFF и старый признак синхронизации. */
clock->port = *port; /* Сохраняет callbacks без выделения динамической памяти. */
clock->device_type = device_type; /* Запоминает тип источника пакета готовности. */
clock->device_id = device_id; /* Запоминает экземпляр источника пакета готовности. */
clock->stamp.weekday = 6U; /* Начальная локальная эпоха 2000-01-01 приходится на субботу при 0=воскресенье. */
return 1; /* Разрешает приложению подключать такт и CAN только после успеха. */
} /* Завершает обязательную явную инициализацию. */
void gt_tick_1ms(gt_clock *clock) /* Выполняет короткий шаг часов из миллисекундного ISR. */
{ /* Защищает разрядные переносы и от вложенных читателей. */
uint16_t state = clock->port.enter(clock->port.user); /* Сохраняет прежний INTM, чтобы выход из ISR не разрешил вложенные IRQ. */
++clock->stamp.uptime_ms; /* Продвигает независимое время работы modulo 2^32 миллисекунд. */
if (++clock->stamp.millisecond == 1000U) { /* Переносит ровно 1000 аппаратных миллисекунд в одну секунду. */
clock->stamp.millisecond = 0U; /* Начинает следующую секунду с нулевой дробной части. */
if (++clock->stamp.seconds % 86400UL == 0UL) clock->stamp.weekday = (clock->stamp.weekday + 1U) % 7U; /* При переходе суток продвигает принятый день недели по циклу из семи дней. */
} /* Завершает редкую секундную ветвь без календарных циклов в ISR. */
clock->port.leave(clock->port.user, state); /* Восстанавливает исходную маску IRQ вызывающего кода. */
} /* Завершает единственный источник продвижения часов. */
void gt_get(gt_clock *clock, gt_stamp *out) /* Снимает согласованные дату, долю секунды и качество синхронизации. */
{ /* Не допускает разрыва 32/64-битных полей на 16-битной шине C28x. */
uint16_t state = clock->port.enter(clock->port.user); /* Блокирует изменение снимка таймером и CAN до завершения копирования. */
*out = clock->stamp; /* Копирует все поля в пределах одной критической секции. */
clock->port.leave(clock->port.user, state); /* Восстанавливает IRQ без безусловного EINT. */
} /* Завершает чтение метки для логгера и тайм-аутов. */
void gt_ready(gt_clock *clock) /* Отмечает фактическое завершение инициализации прикладных подсистем. */
{ /* Разделяет настройку часов и объявление готовности всей платы. */
clock->ready = 1U; /* Повторные вызовы не сбрасывают историю отправок или синхронизаций. */
} /* Передачу сообщения выполнит основной цикл. */
void gt_poll(gt_clock *clock) /* Обслуживает объявление готовности без задержек и ожидания CAN. */
{ /* Разрешён один вызывающий основной цикл; callback send копирует временный кадр. */
gt_stamp now; /* Согласованное время определяет частоту повторов независимо от перевода часов. */
gt_frame frame; /* Локальный пакет не занимает место в постоянной очереди ядра. */
if (!clock->ready) return; /* Плата не сообщает об успехе раньше завершения своей инициализации. */
gt_get(clock, &now); /* Снимает монотонную базу и признак получения RTCSETUP. */
if (clock->announced && (now.synchronized || (uint32_t)(now.uptime_ms - clock->sent_uptime_ms) < GT_READY_RETRY_MS)) return; /* После синхронизации прекращает повторы, сохраняя обязательную первую отправку. */
memset(&frame, 0, sizeof(frame)); /* Явно обнуляет RTR и неиспользуемые октеты сообщения. */
frame.id = 0x18010000UL | ((uint32_t)clock->device_type << 24U) | ((uint32_t)clock->device_id << 20U) | GT_READY_BODY; /* Задаёт стандартный приоритет, Route=1, DISCRETE=1 и FLAGS=3. */
frame.extended = 1U; /* Использует только штатный расширенный идентификатор ProtoCAN. */
frame.dlc = 1U; /* Одного октета хватает для общего флага готовности. */
frame.data[0] = 1U; /* Бит 0 означает завершение инициализации; прочие биты зарезервированы нулями. */
if (clock->port.send(clock->port.user, &frame)) { /* Не считает занятый mailbox успешной отправкой. */
clock->announced = 1U; /* Фиксирует приём транспортом, а не отдельный прикладной ACK мастера. */
clock->sent_uptime_ms = now.uptime_ms; /* Начинает секундный интервал до повтора при отсутствии RTCSETUP. */
} /* При отказе транспорта следующая итерация main повторит попытку без ожидания. */
} /* Завершает неблокирующее обслуживание готовности. */
static int data_frame(const gt_frame *frame) /* Выполняет общую проверку контейнера classic CAN. */
{ /* Не позволяет служебным битам драйвера участвовать в разборе ID. */
return frame && frame->extended == 1U && !frame->remote && frame->id <= 0x1FFFFFFFUL; /* Требует IDE=1, RTR=0 и ровно 29 адресных битов. */
} /* Завершает общую проверку адресного формата. */
int gt_parse_ready(const gt_frame *frame, uint16_t *device_type, uint16_t *device_id) /* Распознаёт на верхнем уровне только определённый пакет готовности. */
{ /* Не путает событие старта с пульсом или произвольными дискретными флагами. */
if (!data_frame(frame) || (frame->id & 0x080FFFFFUL) != (0x08010000UL | GT_READY_BODY) || frame->dlc != 1U || frame->data[0] != 1U) return 0; /* Проверяет Route=1, MsgType=1, Body=0x3000 и единственный разрешённый флаг. */
if (device_type) *device_type = (uint16_t)((frame->id >> 24U) & 7UL); /* Возвращает трёхбитный тип источника при наличии выходного указателя. */
if (device_id) *device_id = (uint16_t)((frame->id >> 20U) & 15UL); /* Возвращает четырёхбитный номер экземпляра источника. */
return 1; /* Верхний уровень может ответить одним общим RTCSETUP всем устройствам. */
} /* Завершает распознавание стартового события мастером. */
int gt_make_rtc(const gt_datetime *date, gt_frame *out) /* Кодирует существующую команду синхронизации для верхнего уровня. */
{ /* Передаёт ровно те семь полей, которые принимает STM32-порт ProtoCAN. */
uint32_t seconds; /* При проверке календаря получает допустимую секундную величину. */
if (!out || !date_seconds(date, &seconds) || date->millisecond != 0U) return 0; /* Не отбрасывает ненулевые миллисекунды молча: проводной формат их не содержит. */
memset(out, 0, sizeof(*out)); /* Нули задают RTR=0, критический приоритет и резерв payload. */
out->id = GT_RTC_ID; /* Передаёт BROADCAST RTCSETUP от верхнего уровня с нулевым параметром команды. */
out->extended = 1U; /* Выбирает расширенный 29-битный CAN-кадр. */
out->dlc = 7U; /* Сохраняет существующий контракт DLC=7 без добавления миллисекундного расширения. */
out->data[0] = date->hour; /* Первый октет содержит часы в двоичном представлении. */
out->data[1] = date->minute; /* Второй октет содержит минуты. */
out->data[2] = date->second; /* Третий октет содержит целые секунды. */
out->data[3] = date->year - 2000U; /* Четвёртый октет хранит смещение года 0..99 от эпохи 2000. */
out->data[4] = date->month; /* Пятый октет содержит месяц 1..12. */
out->data[5] = date->day; /* Шестой октет содержит проверенное число месяца. */
out->data[6] = date->weekday; /* Седьмой октет сохраняет принятый формат дня недели 0..6. */
return 1; /* Кадр готов к неблокирующей отправке транспортом мастера. */
} /* Завершает кодирование общего broadcast. */
int gt_receive(gt_clock *clock, const gt_frame *frame, uint32_t received_uptime_ms) /* Проверяет и применяет RTCSETUP, привязанный к моменту приёма. */
{ /* Отвергает некорректные кадры до изменения состояния устройства. */
gt_datetime date; /* Временный календарь строится из отдельных октетов без struct overlay. */
uint32_t seconds, elapsed, corrected_seconds, whole_days; /* Содержит секунды мастера, задержку в мс, актуальные секунды и перенос дня недели. */
uint16_t i, state; /* Индекс проверки payload и сохранённое состояние IRQ. */
uint16_t corrected_ms; /* Хранит дробную часть отдельно, исключая дорогие 64-битные деления под маской IRQ C28x. */
if (!data_frame(frame) || (frame->id & 0x0FFFFFFFUL) != GT_RTC_ID || frame->dlc != 7U) return 0; /* Принимает оба приоритета, но только Route=0, Type/ID=0, BROADCAST RTCSETUP с параметром 0. */
for (i = 0U; i < 7U; ++i) if (frame->data[i] > 255U) return 0; /* C28x хранит октет в 16-битном элементе; старшие биты означают ошибку адаптера. */
date.hour = frame->data[0]; /* Восстанавливает часы по штатному порядку RTCSETUP. */
date.minute = frame->data[1]; /* Восстанавливает минуты без BCD-преобразований. */
date.second = frame->data[2]; /* Восстанавливает целые секунды мастера. */
date.year = 2000U + frame->data[3]; /* Восстанавливает полный год из двухзначного поля протокола. */
date.month = frame->data[4]; /* Читает номер месяца до проверки таблицы длительности. */
date.day = frame->data[5]; /* Читает число месяца для строгой календарной проверки. */
date.weekday = frame->data[6]; /* Сохраняет присланную мастером фазу недели. */
date.millisecond = 0U; /* Семь октетов RTCSETUP задают точность источника только до секунды. */
if (!date_seconds(&date, &seconds)) return 0; /* Неверные даты не сбрасывают часы и признак предыдущей синхронизации. */
state = clock->port.enter(clock->port.user); /* Защищает сравнение, перенос и публикацию новой временной базы. */
elapsed = clock->stamp.uptime_ms - received_uptime_ms; /* Учитывает время очереди main, в том числе через переполнение uint32_t uptime. */
corrected_seconds = seconds + elapsed / 1000UL; /* Прибавляет полные секунды задержки RX; диапазон входных годов и задержка до 2^32 мс помещаются в uint32_t. */
corrected_ms = (uint16_t)(elapsed % 1000UL); /* Восстанавливает фазу миллисекунд без 64-битного деления в критической секции. */
clock->stamp.correction_ms = ((int64_t)corrected_seconds - clock->stamp.seconds) * 1000LL + corrected_ms - clock->stamp.millisecond; /* Сохраняет знак и полную величину смещения, расширяя разность секунд до умножения. */
whole_days = corrected_seconds / 86400UL - seconds / 86400UL; /* Определяет переходы через полночь за время отложенной обработки. */
clock->stamp.seconds = corrected_seconds; /* Публикует новые секунды независимо от монотонного uptime. */
clock->stamp.millisecond = corrected_ms; /* Публикует долю секунды, соответствующую текущему локальному тику. */
clock->stamp.weekday = (uint16_t)((date.weekday + whole_days) % 7UL); /* Сохраняет правильный день недели даже при обработке после полуночи. */
clock->stamp.sync_uptime_ms = received_uptime_ms; /* Позволяет потребителю вычислить возраст последнего опорного пакета. */
++clock->stamp.generation; /* Отмечает любую принятую коррекцию, включая перевод часов назад. */
clock->stamp.synchronized = 1U; /* Последней записью подтверждает валидность полностью опубликованной даты. */
clock->port.leave(clock->port.user, state); /* Возвращает прежнюю маску прерываний после атомарного обновления. */
return 1; /* Сообщает транспорту об успешной обработке синхронизации. */
} /* Завершает сравнение и немедленную коррекцию часов. */
int gt_calendar(const gt_stamp *stamp, gt_datetime *out) /* Преобразует снимок для человека без блокировки аппаратного таймера. */
{ /* Вызывает календарные циклы только по требованию логгера или интерфейса. */
uint32_t days, remainder; /* Разделяет полные сутки и секунды внутри суток. */
uint16_t year = 2000U, month = 1U, length; /* Начинает разбор от эпохи библиотеки. */
if (!stamp || !out || !stamp->synchronized) return 0; /* Не выдаёт локальный отсчёт до синхронизации за известную календарную дату. */
days = stamp->seconds / 86400UL; /* Выделяет количество завершённых календарных суток. */
remainder = stamp->seconds % 86400UL; /* Сохраняет секунды текущих суток отдельно от календарных вычислений. */
while (days >= (uint32_t)(length = (uint16_t)(365U + (month_days(year, 2U) == 29U)))) { /* Вычитает полные годы с учётом григорианских исключений. */
days -= length; /* Оставляет только сутки после текущего полного года. */
++year; /* Переходит к следующему году, включая работу после 2099 без нового CAN-пакета. */
} /* Завершает поиск календарного года. */
while (days >= (length = month_days(year, month))) { /* Вычитает полные месяцы найденного года. */
days -= length; /* Сохраняет остаток суток внутри следующего месяца. */
++month; /* Продвигает месяц до того, которому принадлежит остаток. */
} /* Завершает поиск календарного месяца. */
out->year = year; /* Возвращает полный календарный год. */
out->month = month; /* Возвращает месяц в привычном диапазоне 1..12. */
out->day = (uint16_t)(days + 1UL); /* Переводит нулевой индекс остатка суток в календарное число. */
out->weekday = stamp->weekday; /* Возвращает фазу недели из согласованного снимка, а не из текущих часов. */
out->hour = (uint16_t)(remainder / 3600UL); /* Выделяет целые часы внутри суток. */
out->minute = (uint16_t)((remainder % 3600UL) / 60UL); /* Выделяет минуты после последнего полного часа. */
out->second = (uint16_t)(remainder % 60UL); /* Возвращает остаток секунд текущей минуты. */
out->millisecond = stamp->millisecond; /* Сохраняет миллисекундную долю исходного снимка. */
return 1; /* Подтверждает наличие календарной синхронизации в исходной метке. */
} /* Завершает календарное представление метки логгера. */
@@ -0,0 +1,26 @@
#ifndef GT_TEST_DSP2833X_DEVICE_H /* Изолирует модель регистров от настоящих заголовков TI. */
#define GT_TEST_DSP2833X_DEVICE_H /* Предотвращает повторное определение тестовых типов. */
#include <stdint.h> /* Использует одинаковые разрядности полей на host-платформе. */
#define EALLOW ((void)0) /* Модель не требует аппаратного разрешения защищённой записи. */
#define EDIS ((void)0) /* Закрытие защищённой записи не меняет модельные регистры. */
#define __interrupt /* Host-компилятор проверяет обычное тело ISR без ABI TI. */
#define M_INT14 0x2000U /* CPU INT14 соответствует биту 13 в IER/IFR TMS320F28335. */
union gt_test_reg { /* Даёт модели 32-битный доступ all и поля, которые использует настоящий порт. */
uint32_t all; /* Имитирует единственный аппаратный доступ к регистру eCAN. */
struct { unsigned DLC:4; unsigned RTR:1; unsigned DBO:1; } bit; /* Для модели важны доступные имена; фактическую раскладку проверяет отдельная TI-компиляция. */
}; /* Завершает минимальную модель регистра. */
struct gt_test_mbox { union gt_test_reg MSGID, MSGCTRL, MDL, MDH; }; /* Выделяет поля mailbox, реально используемые драйвером. */
struct gt_test_mboxes { struct gt_test_mbox MBOX7, MBOX8; }; /* Не предоставляет портам доступ к невыделенным mailbox. */
struct gt_test_ecan { union gt_test_reg CANME, CANMD, CANOPC, CANMIL, CANMIM, CANRMP, CANRML, CANTA, CANAA, CANTRS, CANMC; }; /* Моделирует только задействованные управляющие регистры CAN. */
struct gt_test_lam { union gt_test_reg LAM7; }; /* Предоставляет локальную маску только своего RX. */
struct gt_test_timer { union gt_test_reg TCR, PRD, TPR, TPRH; }; /* Хранит настройку периода и предделителей CPU Timer2. */
struct gt_test_vectors { void (*TINT2)(void); }; /* Позволяет вызвать реальный обработчик порта через установленный им вектор. */
extern struct gt_test_mboxes ECanaMboxes; /* Делит модельные mailbox между портом и тестом. */
extern struct gt_test_ecan ECanaRegs; /* Делит управляющие регистры между кодом порта и проверками. */
extern struct gt_test_lam ECanaLAMRegs; /* Делит состояние аппаратного фильтра. */
extern struct gt_test_timer CpuTimer2Regs; /* Делит конфигурацию автономного миллисекундного таймера. */
extern struct gt_test_vectors PieVectTable; /* Делит назначенный Timer2 вектор. */
extern uint16_t IER, IFR; /* Имитирует CPU-регистры разрешения и ожидания прерываний. */
uint16_t __disable_interrupts(void); /* Предоставляет модель intrinsic с реальным сохранением прежнего состояния. */
void __restore_interrupts(uint16_t state); /* Предоставляет модель точного восстановления IRQ. */
#endif /* Завершает защиту тестовой модели аппаратуры. */
+36
View File
@@ -0,0 +1,36 @@
"""Проверяет ядро и настоящий порт на host; опционально компилирует их TI C28x.""" # Описывает границу проверки без обещания аппаратного испытания.
import argparse # Принимает явные пути TI SDK вместо привязки библиотеки к рабочему компьютеру.
import os # Настраивает PATH компилятора, чтобы Windows находила его DLL.
from pathlib import Path # Строит пути относительно самого тестового набора.
import shutil # Находит GCC или Clang в окружении разработчика.
import subprocess # Запускает компиляторы аргументами без shell-интерполяции.
import tempfile # Изолирует все исполняемые и объектные файлы во временном каталоге.
root = Path(__file__).resolve().parents[1] # Определяет корень c/global-time независимо от текущего каталога.
parser = argparse.ArgumentParser(description=__doc__) # Показывает назначение host- и TI-проверок при --help.
parser.add_argument("--cc", default=os.environ.get("CC")) # Разрешает явно выбрать совместимый host-компилятор.
parser.add_argument("--ti-cgt", type=Path) # Принимает каталог TI C2000 compiler с bin/cl2000 и include.
parser.add_argument("--ti-headers", type=Path) # Принимает каталог с настоящим DSP2833x_Device.h проекта или TI SDK.
parser.add_argument("--ti-common", type=Path) # Принимает дополнительный каталог DSP2833x_GlobalPrototypes.h из DSP2833x_common/include.
args = parser.parse_args() # Проверяет параметры до создания любых файлов сборки.
candidates = [args.cc, shutil.which("gcc"), shutil.which("clang"), "C:/msys64/ucrt64/bin/gcc.exe"] # Последний путь обеспечивает привычное Windows-окружение без изменения глобального PATH.
compiler = next((str(Path(p).resolve()) for p in candidates if p and Path(p).is_file()), None) # Выбирает первый существующий компилятор, сохраняя явный --cc в приоритете.
if not compiler: # Не заменяет отсутствие компилятора успешным пустым запуском.
parser.error("Укажите --cc с путём к GCC/Clang") # Даёт воспроизводимое требование для запуска регрессий.
if bool(args.ti_cgt) != bool(args.ti_headers): # TI проверка требует одновременно компилятор и аппаратные заголовки.
parser.error("--ti-cgt и --ti-headers нужно указать вместе") # Предотвращает случайную проверку порта с host-моделью вместо SDK.
env = dict(os.environ) # Сохраняет исходное окружение остальных инструментов.
env["PATH"] = str(Path(compiler).parent) + os.pathsep + env.get("PATH", "") # Добавляет соседние DLL GCC только в дочерние процессы тестов.
with tempfile.TemporaryDirectory(prefix="global-time-") as output: # Удаляет только собственный временный каталог после завершения проверок.
for name, sources, includes in [("core", ["src/global_time.c", "tests/test_global_time.c"], ["include"]), ("f28335", ["src/global_time.c", "ports/tms320f28335/global_time_f28335.c", "tests/test_f28335.c"], ["include", "ports/tms320f28335", "tests/f28335_stubs"])]: # Проверяет ядро отдельно и реальный порт с модельными регистрами.
exe = Path(output) / (name + (".exe" if os.name == "nt" else "")) # Сохраняет host-программы за пределами исходного дерева.
command = [compiler, "-std=c99", "-Wall", "-Wextra", "-Werror", "-pedantic", *["-I" + str(root / p) for p in includes], *[str(root / p) for p in sources], "-o", str(exe)] # Считает все предупреждения ошибками и использует реальные исходники библиотеки.
subprocess.run(command, check=True, env=env, timeout=60) # Не маскирует ошибку компиляции успешным запуском старого EXE.
subprocess.run([str(exe)], check=True, env=env, timeout=30) # Выполняет календарные и аппаратные сценарии с ограниченным временем.
if args.ti_cgt: # Дополнительная проверка нужна для ABI с 16-битным char и настоящих регистров TI.
ti = args.ti_cgt.resolve() # Фиксирует абсолютный корень выбранного SDK компилятора.
for source in ["src/global_time.c", "ports/tms320f28335/global_time_f28335.c"]: # Компилирует оба поставляемых MCU исходника без изменения прошивки потребителя.
obj = Path(output) / (Path(source).stem + ".obj") # Разделяет объектные файлы ядра и аппаратного адаптера.
include_paths = [root / "include", root / "ports/tms320f28335", args.ti_headers.resolve(), ti / "include", *([args.ti_common.resolve()] if args.ti_common else [])] # Отдельно подключает common-заголовки старого TI SDK, не смешивая их с host-моделью.
command = [str(ti / "bin/cl2000.exe"), "--compile_only", "-v28", "-ml", "-mt", "--float_support=fpu32", "--abi=coffabi", "--c99", "-O2", "--opt_for_speed=0", "--display_error_number", *["--include_path=" + str(p) for p in include_paths], "--output_file=" + str(obj), str(root / source)] # Использует large/COFF ABI действующего проекта F28335 и штатные заголовки.
subprocess.run(command, check=True, timeout=60) # Ошибка TI C28x останавливает проверку вместо объявления host-модели достаточной.
print("global-time: TI C28x large/COFF compilation OK") # Подтверждает именно компиляцию объектов, не аппаратные испытания или полную линковку прошивки.
+88
View File
@@ -0,0 +1,88 @@
#include "DSP2833x_Device.h" /* Использует host-модель регистров, а не альтернативную реализацию драйвера. */
#include "global_time_f28335.h" /* Тестирует публичные hooks настоящего аппаратного порта. */
#ifdef NDEBUG /* Не позволяет Release-сборке превратить аппаратные проверки в пустую программу. */
#undef NDEBUG /* Включает assert независимо от флагов конфигурации CMake. */
#endif /* Завершает настройку обязательных проверяющих утверждений. */
#include <assert.h> /* Проверяет сохранность соседних ресурсов, обработку CAN и период Timer2. */
#include <stdio.h> /* Выводит краткое подтверждение проверенных сценариев. */
struct gt_test_mboxes ECanaMboxes; /* Выделяет модельные MBOX7 и MBOX8. */
struct gt_test_ecan ECanaRegs; /* Выделяет модель управляющих регистров eCAN-A. */
struct gt_test_lam ECanaLAMRegs; /* Выделяет локальную маску RX. */
struct gt_test_timer CpuTimer2Regs; /* Выделяет модель CPU Timer2. */
struct gt_test_vectors PieVectTable; /* Хранит реальный адрес ISR, назначенный портом. */
uint16_t IER, IFR; /* Выделяет модель CPU-масок INT14 и соседних прерываний. */
static uint16_t masked; /* Сохраняет модельное состояние глобальной маски INTM. */
uint16_t __disable_interrupts(void) /* Моделирует атомарный запрет IRQ и возвращаемый предыдущий INTM. */
{ /* Повторный запрет из ISR обязан вернуть уже установленную маску. */
uint16_t old = masked; /* Сохраняет прежнюю маску до её изменения. */
masked = 1U; /* Имитирует запрет маскируемых IRQ. */
return old; /* Позволяет реальному порту точно восстановить предыдущий режим. */
} /* Завершает модель intrinsic запрета. */
void __restore_interrupts(uint16_t state) /* Восстанавливает маску, полученную соответствующим входом. */
{ /* Проверяет, что порт действительно вызвал вход в критическую секцию. */
assert(masked == 1U); /* Восстановление без предшествующего запрета нарушило бы вложенность. */
masked = state; /* Сохраняет исходный ISR-контекст без безусловного разрешения IRQ. */
} /* Завершает модель intrinsic восстановления. */
static void inject(const gt_frame *frame) /* Доставляет wire-данные в аппаратный RX и вызывает настоящий ISR hook. */
{ /* Не вызывает gt_receive напрямую, проверяя весь путь распаковки MDL/MDH. */
uint16_t i; /* Перебирает восемь проводных октетов фиксированного classic CAN. */
ECanaMboxes.MBOX7.MSGID.all = 0xC0000000UL | frame->id; /* Имитирует расширенный ID и включённую локальную фильтрацию eCAN. */
ECanaMboxes.MBOX7.MSGCTRL.all = frame->dlc | ((uint32_t)frame->remote << 4U); /* Укладывает DLC и RTR в модельный регистр сообщения. */
ECanaMboxes.MBOX7.MDL.all = ECanaMboxes.MBOX7.MDH.all = 0UL; /* Сбрасывает старый payload перед имитацией нового приёма. */
for (i = 0U; i < 8U; ++i) { /* Укладывает данные младшим октетом вперёд, как eCAN при DBO=0. */
if (i < 4U) ECanaMboxes.MBOX7.MDL.all |= (uint32_t)frame->data[i] << (8U * i); /* Первые четыре октета занимают MDL. */
else ECanaMboxes.MBOX7.MDH.all |= (uint32_t)frame->data[i] << (8U * (i - 4U)); /* Следующие четыре октета занимают MDH. */
} /* Завершает аппаратное размещение кадра. */
ECanaRegs.CANRMP.all = 0x82UL; /* Выставляет RX7 и независимый pending legacy mailbox1. */
gt_f28335_rx_isr(); /* Захватывает временную привязку и кадр в очередь настоящего порта. */
assert(ECanaRegs.CANRMP.all == 0x80UL); /* Запись W1C должна содержать только бит собственного mailbox, сохраняя pending соседа на аппаратуре. */
ECanaRegs.CANRMP.all = 2UL; /* Вручную применяет W1C к обычной RAM модели: RX7 снят, RX1 сохранён. */
} /* Завершает модель получения кадра без подмены алгоритма порта. */
int main(void) /* Проверяет аппаратную настройку и общий путь ISR → main → метка логгера. */
{ /* Модель не утверждает электрические тайминги или CAN ACK настоящего трансивера. */
gt_stamp stamp; /* Читает время через тот же атомарный API, которым будет пользоваться логгер. */
gt_datetime date = {2026U, 10U, 19U, 1U, 12U, 34U, 56U, 0U}; /* Независимый календарный эталон: понедельник 19 октября 2026 года. */
gt_frame frame; /* Содержит broadcast мастера для доставки через модель eCAN. */
uint16_t i; /* Управляет известным числом тиков до обработки кадра в main. */
gt_f28335_tick_1ms(); gt_f28335_rx_isr(); gt_f28335_service(); /* Ранние hooks до init обязаны безопасно завершаться. */
ECanaRegs.CANME.all = 0x7FUL; /* Резервирует существующие mailbox0..6 для legacy CAN, Boot и pulse. */
ECanaRegs.CANMD.all = 0xFFFFFFFAUL; /* Воспроизводит исходные направления legacy mailbox проекта 28335_periph. */
ECanaRegs.CANMIM.all = 0x4AUL; /* Имитирует разрешённые RX1/RX3/RX6 других CAN-сервисов. */
ECanaRegs.CANMIL.all = 0x40UL; /* Фиксирует чужую маршрутизацию IRQ, которую порт обязан сохранить. */
assert(!gt_f28335_init_periph(32U)); /* Некорректный физический адрес не должен открыть новый CAN-сервис. */
assert(gt_f28335_init_periph(5U)); /* Настраивает УМП1 как PERIPH5, Type1/ID5. */
assert(ECanaRegs.CANME.all == 0x1FFUL && (ECanaRegs.CANMD.all & 0x7FUL) == 0x7AUL); /* Проверяет добавление только mailbox7/8 и сохранность чужих направлений. */
assert(ECanaRegs.CANMIM.all == 0xCAUL && ECanaRegs.CANMIL.all == 0x40UL); /* Проверяет RX7 IRQ line0 без изменения RX1/RX3/RX6. */
assert(ECanaMboxes.MBOX7.MSGID.all == 0xC0000030UL && ECanaLAMRegs.LAM7.all == 0x10000000UL); /* Фильтр принимает только время мастера с любым приоритетом. */
ECanaRegs.CANTA.all = ECanaRegs.CANAA.all = ECanaRegs.CANRMP.all = 0UL; /* Применяет к RAM модели W1C, выполненные init на реальной аппаратуре. */
gt_f28335_service(); /* Проверяет первый проход main при отсутствии входящего кадра. */
assert(ECanaRegs.CANTRS.all == 0UL); /* Сам init не должен объявить готовность прикладной платы. */
gt_f28335_ready(); gt_f28335_service(); /* Публикует прикладной успех и запускает передачу из main. */
assert(ECanaRegs.CANTRS.all == 0x100UL && ECanaMboxes.MBOX8.MSGID.all == 0x99513000UL && ECanaMboxes.MBOX8.MDL.all == 1UL); /* Проверяет точный аппаратный запрос READY с правильным адресом. */
assert(gt_make_rtc(&date, &frame)); /* Строит штатный RTCSETUP без аппаратного RTC мастера в тесте. */
inject(&frame); /* Проверяет разбор всех календарных полей через настоящий RX ISR. */
for (i = 0U; i < 37U; ++i) gt_f28335_tick_1ms(); /* Имитирует 37 мс задержки main при продолжающемся такте. */
gt_f28335_service(); /* Применяет отложенный RX и сохраняет привязку к исходному моменту приёма. */
gt_get(gt_f28335_clock(), &stamp); /* Снимает тот же экземпляр часов, что обслуживает порт. */
assert(gt_calendar(&stamp, &date) && date.year == 2026U && date.month == 10U && date.day == 19U && date.hour == 12U && date.minute == 34U && date.second == 56U && date.millisecond == 37U); /* Проверяет календарь и компенсацию задержки всего пути приёма. */
assert(stamp.sync_uptime_ms == 0UL && stamp.uptime_ms == 37UL); /* Очередь не должна подменить время RX временем обработки main. */
ECanaRegs.CANTA.all = 0x100UL; ECanaRegs.CANTRS.all = 0UL; /* Имитирует аппаратный ACK ранее отправленной готовности. */
gt_f28335_service(); /* Освобождает собственный TX без повторной отправки после синхронизации. */
ECanaRegs.CANTA.all = ECanaRegs.CANAA.all = 0UL; /* Применяет аппаратное W1C к RAM модели ACK. */
assert(ECanaRegs.CANTRS.all == 0UL); /* Синхронизация прекращает повторную стартовую рассылку. */
IER = 0x1001U; IFR = 0x3001U; /* Подготавливает чужие CPU IRQ и старый pending INT14. */
assert(!gt_f28335_timer2_start(150000001UL)); /* Некратная 1000 частота не должна давать скрытый дробный период. */
assert(gt_f28335_timer2_start(150000000UL)); /* Настраивает аппаратные 1 мс для типичной частоты F28335 150 МГц. */
assert(CpuTimer2Regs.PRD.all == 149999UL && CpuTimer2Regs.TPR.all == 0UL && CpuTimer2Regs.TPRH.all == 0UL); /* Проверяет включительный период PRD..0 и отсутствие предделителей. */
assert(IER == 0x3001U && IFR == 0x1001U && CpuTimer2Regs.TCR.all == 0xC020UL); /* Сохраняет чужие CPU маски и запускает только Timer2. */
PieVectTable.TINT2(); /* Вызывает реальное тело ISR через вектор, назначенный портом. */
gt_get(gt_f28335_clock(), &stamp); /* Проверяет единственный миллисекундный шаг собственного ISR. */
assert(stamp.uptime_ms == 38UL && stamp.millisecond == 38U); /* Timer2 должен добавлять ровно один тик на прерывание. */
assert(!gt_f28335_init_periph(6U)); /* Повторный init не должен потерять владение работающим Timer2. */
gt_f28335_timer2_stop(); /* Освобождает ранее захваченный аппаратный такт. */
assert(IER == 0x1001U && CpuTimer2Regs.TCR.all == 0x8010UL && masked == 0U); /* Остановка сохраняет чужие IRQ и исходный глобальный INTM. */
ECanaRegs.CANMC.bit.DBO = 1U; /* Имитирует несовместимый порядок октетов контроллера. */
assert(!gt_f28335_init_periph(6U)); /* Не позволяет молча декодировать другую аппаратную раскладку payload. */
puts("global-time F28335: eCAN isolation, RX delay, ACK and Timer2 OK"); /* Выводит компактный результат аппаратных сценариев. */
return 0; /* Завершает проверку после всех обязательных утверждений. */
} /* Завершает host-регрессию порта TI F28335. */
+145
View File
@@ -0,0 +1,145 @@
#include "global_time.h" /* Проверяет настоящий переносимый код часов без подмены его реализации. */
#ifdef NDEBUG /* Проверки должны выполняться и при Release-сборке CMake. */
#undef NDEBUG /* Сохраняет assert активным независимо от выбранного уровня оптимизации. */
#endif /* Завершает принудительное включение проверяющих утверждений. */
#include <assert.h> /* Останавливает тест при нарушении временного или проводного контракта. */
#include <stdio.h> /* Выводит компактное подтверждение выполнения набора проверок. */
#include <string.h> /* Позволяет воспроизвести ненулевую RAM после тёплого старта. */
static uint16_t irq_mask, depth; /* Моделирует исходную маску IRQ и вложенность критических секций. */
static int accepted, sends; /* Управляет занятостью транспорта и считает попытки объявления готовности. */
static gt_frame sent; /* Сохраняет последний кадр для независимой проверки ID и payload. */
static uint16_t enter(void *user) /* Моделирует TI intrinsic с сохранением предыдущего INTM. */
{ /* Проверяет, что ядро пользуется единым механизмом защиты состояния. */
uint16_t previous = irq_mask; /* Сохраняет маску до входа в текущий уровень блокировки. */
assert(user == &sends); /* Подтверждает неизменную передачу пользовательского контекста. */
irq_mask = 1U; /* Запрещает модельным прерываниям менять данные до leave. */
++depth; /* Отмечает вложенный уровень для контроля парности входа и выхода. */
return previous; /* Возвращает исходное состояние, включая вызов из уже закрытого ISR. */
} /* Завершает модель входа в критическую секцию. */
static void leave(void *user, uint16_t previous) /* Проверяет восстановление предыдущей маски без безусловного разрешения. */
{ /* Делает нарушения вложенности видимыми в host-тесте. */
assert(user == &sends && depth > 0U && irq_mask == 1U); /* Выход допустим только из реально захваченной секции. */
--depth; /* Освобождает один уровень модельной блокировки. */
irq_mask = previous; /* Возвращает именно исходную маску вызывающего кода. */
} /* Завершает проверяемую модель выхода. */
static int send(void *user, const gt_frame *frame) /* Моделирует неблокирующую передачу и копирование временного payload. */
{ /* Callback не должен выполняться с длинной критической секцией ядра. */
assert(user == &sends && depth == 0U); /* Проверяет передачу контекста и отсутствие блокировки IRQ на время отправки. */
++sends; /* Считает и принятые, и отклонённые транспортом попытки. */
sent = *frame; /* Сохраняет независимую копию до возврата callback. */
return accepted; /* Имитирует занятость mailbox без ожидания. */
} /* Завершает модель транспорта. */
static void advance(gt_clock *clock, uint32_t milliseconds) /* Продвигает реальный алгоритм через границы календарных полей. */
{ /* Пошаговый вызов также проверяет миллисекундные переносы в ISR. */
while (milliseconds--) gt_tick_1ms(clock); /* Не подменяет поведение таймера прямой правкой абсолютного времени. */
} /* Завершает управляемое продвижение времени. */
static gt_frame rtc(uint16_t year, uint16_t month, uint16_t day, uint16_t hour, uint16_t minute, uint16_t second) /* Строит календарный эталон кодировщиком верхнего уровня. */
{ /* Ожидаемые значения времени в тестах проверяются отдельно от кодировщика. */
gt_datetime date = {year, month, day, 6U, hour, minute, second, 0U}; /* День недели сохраняется как допустимая фаза цикла, исходная суббота=6. */
gt_frame frame; /* Получает полноценный расширенный CAN кадр. */
assert(gt_make_rtc(&date, &frame)); /* Корректный календарь обязан кодироваться в существующие семь октетов. */
return frame; /* Возвращает независимый контейнер для изменения ошибочных полей. */
} /* Завершает построение тестовых опорных кадров. */
int main(void) /* Проверяет календарь, коррекции, задержки, переполнения и стартовый CAN-обмен. */
{ /* Использует только публичный API, кроме целевой установки границы uptime. */
gt_clock clock; /* Состояние заполняется мусором, имитируя startup без очистки .ebss. */
gt_port port = {&sends, enter, leave, send}; /* Подключает проверяющие блокировки и управляемый транспорт. */
gt_stamp stamp, before; /* Два атомарных снимка позволяют проверять отсутствие побочных изменений при ошибке. */
gt_datetime date; /* Календарный результат проверяется по известным границам дат. */
gt_frame frame, bad; /* Исходный корректный кадр и копия для отрицательных проверок. */
uint16_t type, id, index; /* Полученные адресные поля и индекс проверки каждого октета. */
const uint16_t invalid[7] = {24U, 60U, 60U, 100U, 0U, 0U, 7U}; /* Независимые граничные ошибки каждого календарного поля на линии. */
memset(&clock, 0xA5, sizeof(clock)); /* Воспроизводит остатки RAM вместо неявной инициализации нулями хоста. */
assert(!gt_init(&clock, &port, 8U, 0U)); /* Запрещает трёхбитному типу переполниться в направление Route. */
assert(!gt_init(&clock, &port, 1U, 16U)); /* Запрещает молчаливое обрезание экземпляра до адреса другой платы. */
assert(gt_init(&clock, &port, 1U, 5U)); /* Создаёт УМП1 по действующему реестру PERIPH. */
gt_get(&clock, &stamp); /* Проверяет результат явной инициализации всей временной базы. */
assert(stamp.seconds == 0UL && stamp.uptime_ms == 0UL && !stamp.synchronized && stamp.generation == 0UL); /* До синхронизации календарь не должен выглядеть достоверным. */
assert(!gt_calendar(&stamp, &date)); /* Не позволяет приложению выдавать локальную эпоху за полученную дату. */
advance(&clock, 1001UL); /* Пересекает первую секундную границу программного таймера. */
gt_get(&clock, &stamp); /* Снимает дробную часть после переноса секунды. */
assert(stamp.seconds == 1UL && stamp.millisecond == 1U && stamp.uptime_ms == 1001UL); /* Проверяет ровно 1000 тиков на секунду без ошибки на единицу. */
gt_poll(&clock); /* Пытается обслужить отправку до завершения инициализации платы. */
assert(sends == 0); /* Пакет готовности запрещён до явного ready. */
gt_ready(&clock); /* Разрешает стартовый пакет только после прикладной готовности. */
gt_poll(&clock); /* Имитирует занятую CAN передачу при accepted=0. */
assert(sends == 1 && !clock.announced); /* Занятость транспорта не теряет обязательный пакет инициализации. */
accepted = 1; /* Освобождает транспорт на следующем проходе main. */
gt_poll(&clock); /* Повторяет отклонённую попытку без задержки и блокировки IRQ. */
assert(sends == 2 && sent.id == 0x19513000UL && sent.dlc == 1U && sent.data[0] == 1U); /* Фиксирует проводной эталон READY узла Type=1, ID=5. */
assert(gt_parse_ready(&sent, &type, &id) && type == 1U && id == 5U); /* Мастер должен узнать физический адрес стартовавшей платы. */
bad = sent; /* Готовит отрицательный вариант стартового сообщения. */
bad.id ^= 0x08000000UL; /* Меняет направление на запрос мастера, сохранив payload. */
assert(!gt_parse_ready(&bad, 0, 0)); /* Исключает распознавание собственного запроса как готовности платы. */
advance(&clock, 999UL); /* Проверяет ограничение частоты повторов перед секундной границей. */
gt_poll(&clock); /* Обслуживает CAN на последней миллисекунде запрета повтора. */
assert(sends == 2); /* Готовность не должна засорять шину на каждом проходе main. */
advance(&clock, 1UL); /* Достигает точно секундного интервала повторной отправки. */
gt_poll(&clock); /* Повторяет событие старта при отсутствии ответа временем. */
assert(sends == 3); /* Потерянный ответ мастера должен приводить к повтору объявления готовности. */
frame = rtc(2024U, 2U, 28U, 23U, 59U, 59U); /* Задаёт известную границу високосного февраля. */
assert(frame.id == 0x30UL && frame.dlc == 7U && frame.data[3] == 24U && frame.data[4] == 2U); /* Проверяет совместимый ID и календарный wire format. */
assert(gt_receive(&clock, &frame, clock.stamp.uptime_ms)); /* Устанавливает первую реальную календарную дату. */
advance(&clock, 1001UL); /* Пересекает полночь на один миллисекундный тик. */
gt_get(&clock, &stamp); /* Снимает календарную метку после перехода даты. */
assert(gt_calendar(&stamp, &date) && date.year == 2024U && date.month == 2U && date.day == 29U && date.hour == 0U && date.millisecond == 1U); /* Проверяет 29 февраля и сброс часов/минут/секунд на полночь. */
assert(stamp.weekday == 0U && stamp.generation == 1UL); /* День недели циклически продвигается, а тики не создают новые поколения коррекции. */
assert(gt_timestamp_ms(&stamp) > UINT32_MAX); /* Календарные миллисекунды остаются широкими и не обрезаются до uptime. */
advance(&clock, 2000UL); /* Даёт мастеру время ответить; уже синхронизированный узел не повторяет READY. */
gt_poll(&clock); /* Проверяет прекращение стартовых повторов после RTCSETUP. */
assert(sends == 3); /* Получение календаря завершает повторный запрос времени. */
before = stamp; /* Сохраняет известную метку для проверки отрицательной коррекции. */
frame = rtc(2000U, 1U, 1U, 0U, 0U, 0U); /* Задаёт независимый эталон нулевой секунды эпохи библиотеки. */
assert(gt_receive(&clock, &frame, clock.stamp.uptime_ms)); /* Применяет разрешённый перевод глобальных часов назад. */
gt_get(&clock, &stamp); /* Снимает результат атомарной коррекции. */
assert(stamp.seconds == 0UL && stamp.millisecond == 0U && stamp.correction_ms < 0 && stamp.uptime_ms > before.uptime_ms && stamp.generation == 2UL); /* Проверяет независимость тайм-аутов от отрицательной календарной коррекции. */
assert(gt_timestamp_ms(&stamp) == 0ULL); /* Фиксирует точное начало принятой эпохи. */
before = stamp; /* Сохраняет состояние до серии ошибочных CAN кадров. */
for (index = 0U; index < 7U; ++index) { /* Проверяет наличие лишних старших битов в каждом слове, содержащем октет C28x. */
bad = frame; /* Восстанавливает исходный корректный RTCSETUP перед каждой ошибкой. */
bad.data[index] = 256U; /* Значение вне одного проводного октета не должно обрезаться до нуля. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Отвергает ошибку порта до изменения даты. */
bad = frame; /* Возвращает корректный исходный кадр для проверки допустимого диапазона поля. */
bad.data[index] = invalid[index]; /* Подставляет двоичное, но календарно недопустимое значение. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Исключает нормализацию ошибочного времени и выход за границы таблицы месяцев. */
} /* Завершает проверку всей широкой wire-границы. */
bad = frame; bad.extended = 0U; /* Подготавливает стандартный 11-битный кадр с похожим ID. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* ProtoCAN требует IDE=1 независимо от численного ID. */
bad = frame; bad.remote = 1U; /* Подготавливает RTR без реальных календарных данных. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Удалённый запрос не устанавливает глобальное время. */
bad = frame; bad.dlc = 8U; /* Добавляет неописанный восьмой октет без согласованного расширения протокола. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Длина существующего RTCSETUP должна быть ровно семь октетов. */
bad = frame; bad.id |= 0x01000000UL; /* Имитирует broadcast от чужого типа устройства вместо верхнего уровня. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Исключает коррекцию по произвольным сообщениям другой платы. */
bad = frame; bad.data[4] = 2U; bad.data[5] = 30U; /* Подготавливает несуществующее 30 февраля високосного года. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Календарные ошибки не нормализуются в другую дату. */
bad = frame; bad.data[3] = 23U; bad.data[4] = 2U; bad.data[5] = 29U; /* Подготавливает 29 февраля невисокосного года. */
assert(!gt_receive(&clock, &bad, before.uptime_ms)); /* Отвергает некорректную дату даже при допустимых диапазонах отдельных полей. */
gt_get(&clock, &stamp); /* Проверяет итоговое состояние после всех отклонённых кадров. */
assert(stamp.seconds == before.seconds && stamp.generation == before.generation && stamp.synchronized); /* Неверные пакеты не теряют последнюю достоверную синхронизацию. */
before.seconds = 3160857600UL; /* Независимый календарный эталон: ровно 36584 суток от 2000-01-01 до 2100-03-01. */
assert(gt_calendar(&before, &date) && date.year == 2100U && date.month == 3U && date.day == 1U); /* Проверяет исключение невисокосного 2100 года при продолжении локального хода часов. */
clock.stamp.uptime_ms = UINT32_MAX - 4UL; /* Целевым образом размещает монотонный счётчик около переполнения 49,7 суток. */
advance(&clock, 10UL); /* Пересекает modulo-границу до обработки уже полученного кадра. */
frame.id |= 0x10000000UL; /* Проверяет допустимость стандартного приоритета RTCSETUP наряду с критическим. */
assert(gt_receive(&clock, &frame, UINT32_MAX - 4UL)); /* Передаёт момент RX до переполнения, обработку — после. */
gt_get(&clock, &stamp); /* Снимает скорректированную фазу доли секунды. */
assert(stamp.seconds == 0UL && stamp.millisecond == 10U && stamp.uptime_ms == 5UL); /* Компенсация очереди должна быть 10 мс, независимо от переполнения uptime. */
frame = rtc(2023U, 12U, 31U, 23U, 59U, 59U); /* Проверяет смену календарного года при отложенной обработке. */
before = stamp; /* Запоминает момент RX непосредственно перед задержкой основного цикла. */
advance(&clock, 1250UL); /* Имитирует долгую прикладную операцию при продолжающемся аппаратном таймере. */
assert(gt_receive(&clock, &frame, before.uptime_ms)); /* Применяет время по моменту RX, а не по моменту выхода из очереди. */
gt_get(&clock, &stamp); /* Снимает дату после учёта задержки обработки. */
assert(gt_calendar(&stamp, &date) && date.year == 2024U && date.month == 1U && date.day == 1U && date.second == 0U && date.millisecond == 250U); /* Проверяет перенос секунды, суток и года с сохранением 250 мс. */
irq_mask = 1U; /* Имитирует вызов логгера из ISR с уже запрещёнными прерываниями. */
gt_get(&clock, &stamp); /* Проверяет чтение метки при вложенной критической секции. */
gt_tick_1ms(&clock); /* Проверяет продвижение часов при исходном INTM=1. */
assert(irq_mask == 1U && depth == 0U); /* Выходы не должны случайно разрешить IRQ вызывающего обработчика. */
irq_mask = 0U; /* Восстанавливает модельное окружение main перед новой инициализацией. */
assert(gt_init(&clock, &port, 2U, 15U)); /* Проверяет холодный reset качества и верхний адрес PERIPH31. */
assert(gt_receive(&clock, &frame, 0UL)); /* Имитирует broadcast ещё до прикладного ready. */
gt_ready(&clock); gt_ready(&clock); gt_poll(&clock); /* Повторный ready должен быть идемпотентным, но первая отправка всё равно обязательна. */
assert(sends == 4 && gt_parse_ready(&sent, &type, &id) && type == 2U && id == 15U); /* Ранняя синхронизация не подавляет сообщение об успешной инициализации платы. */
puts("global-time: calendar, CAN, correction, IRQ and rollover OK"); /* Возвращает краткое свидетельство пройденных смысловых сценариев. */
return 0; /* Завершает host-тест только после всех обязательных проверок. */
} /* Завершает независимую проверку переносимого ядра. */
+15 -1
View File
@@ -28,7 +28,7 @@
| `OFF` / `ON` | постоянно выключен / включён |
| `STARTUP` | три коротких импульса, затем постоянно включён |
| `WORK` | 100 мс включён, 900 мс выключен |
| `ACTIVITY` | 50/50 мс |
| `ACTIVITY` | один цикл: 100 мс включён, 900 мс выключен, затем остаётся выключенным |
| `WARNING` | 250/250 мс |
| `ERROR` | два импульса и пауза |
| `CRITICAL` | три импульса и пауза |
@@ -86,6 +86,20 @@ for (;;) {
не начинает рисунок заново. Для нового импульса события служит
`LedIndicator_RestartMode`.
Для индикации приёма вызывайте `LedIndicator_TriggerActivity` либо
`LedIndicator_TriggerActivityAt` на каждый принятый пакет. Свечение занимает
10% секундного цикла: 100 мс включён и 900 мс выключен. Новый приём немедленно
перезапускает фазу свечения, даже внутри предыдущего импульса или паузы;
очереди циклов нет. При интервалах между пакетами меньше 100 мс LED может
гореть непрерывно до истечения 100 мс после последнего пакета. Без нового
события `ACTIVITY` не повторяется. Настраиваемая таблица `patterns` по-прежнему
позволяет изменить длительности и повторение для других применений.
```c
LedIndicator_TriggerActivityAt(&led, 0U, received_ms); /* Каждый приём запускает цикл канала 0 от миллисекундной метки события. */
LedIndicator_ProcessAt(&led, now_ms); /* Главный цикл обслуживает окончание свечения без задержек и ожидания следующего пакета. */
```
## STM32F103, STM32F4, STM32G431 и STM32G474
Порт `ports/stm32-hal` принимает конкретный `TIM_HandleTypeDef *`; библиотека
+11 -1
View File
@@ -18,7 +18,7 @@ static const LedIndicator_Pattern g_default_patterns[LED_INDICATOR_MODE_COUNT] =
{{0U}, LEVELS_1, 1U, 1U, 1U},
{{100U, 100U, 100U, 100U, 100U, 100U}, LEVELS_101010, 6U, 0U, 1U},
{{100U, 900U}, LEVELS_10, 2U, 1U, 0U},
{{50U, 50U}, LEVELS_10, 2U, 1U, 0U},
{{100U, 900U}, LEVELS_10, 2U, 0U, 0U}, /* Один цикл приёма длится 1000 мс: 100 мс свечения составляют 10%, затем 900 мс паузы и выключение. */
{{250U, 250U}, LEVELS_10, 2U, 1U, 0U},
{{100U, 100U, 100U, 700U}, LEVELS_1010, 4U, 1U, 0U},
{{100U, 100U, 100U, 100U, 100U, 700U}, LEVELS_101010, 6U, 1U, 0U}
@@ -220,6 +220,16 @@ uint8_t LedIndicator_RestartMode(LedIndicator *instance, size_t channel,
instance->port.now_ms(instance->port.context));
}
uint8_t LedIndicator_TriggerActivityAt(LedIndicator *instance, size_t channel, uint32_t now_ms) /* Каждый пакет начинает ACTIVITY заново, даже если предыдущая вспышка ещё продолжается. */
{ /* Использует общую проверку канала и принудительный сброс фазы в миллисекундной временной базе. */
return LedIndicator_RestartModeAt(instance, channel, LED_INDICATOR_MODE_ACTIVITY, now_ms); /* Штатный однократный шаблон завершает цикл выключенным уровнем без свободного повторения. */
} /* Завершает запуск одного цикла приёма без ожидания окончания свечения либо паузы. */
uint8_t LedIndicator_TriggerActivity(LedIndicator *instance, size_t channel) /* Предоставляет тот же перезапуск по событию с использованием часов аппаратного порта. */
{ /* Сохраняет поведение варианта At для приложения без собственного передаваемого времени. */
return LedIndicator_RestartMode(instance, channel, LED_INDICATOR_MODE_ACTIVITY); /* Часы порта считываются штатным API один раз на зарегистрированный приём. */
} /* Завершает неблокирующий запуск цикла приёма с аппаратным источником времени. */
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms)
{
size_t i;
+6
View File
@@ -114,6 +114,12 @@ void LedIndicator_Process(LedIndicator *instance);
/** Обновляет выходы с явно переданной меткой времени. */
void LedIndicator_ProcessAt(LedIndicator *instance, uint32_t now_ms);
/** Начинает один цикл ACTIVITY на каждый приём; новый вызов немедленно перезапускает свечение, не создавая очередь. */
uint8_t LedIndicator_TriggerActivityAt(LedIndicator *instance, size_t channel, uint32_t now_ms); /* Стандартный цикл: 100 мс включён, 900 мс выключен; время задаётся в миллисекундах. */
/** Использует часы аппаратного порта для такого же перезапуска цикла при каждом приёме. */
uint8_t LedIndicator_TriggerActivity(LedIndicator *instance, size_t channel); /* Вызывать из контекста Process; передачу событий из ISR обеспечивает приложение. */
LedIndicator_Mode LedIndicator_GetMode(const LedIndicator *instance, size_t channel);
#ifdef __cplusplus
+21 -1
View File
@@ -85,6 +85,26 @@ int main(void)
CHECK(LedIndicator_SetMode(&indicator, 3U, LED_INDICATOR_MODE_ON) == 0U);
CHECK(LedIndicator_GetMode(&indicator, 0U) == LED_INDICATOR_MODE_WORK);
puts("led indicator tests passed");
CHECK(patterns[LED_INDICATOR_MODE_ACTIVITY].duration_ms[0] == 100U && patterns[LED_INDICATOR_MODE_ACTIVITY].duration_ms[1] == 900U && patterns[LED_INDICATOR_MODE_ACTIVITY].repeat == 0U); /* Фиксирует согласованные 10% свечения и один цикл на приём в канонической таблице. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 2500U) != 0U && fake.outputs[0] == 1U); /* Приём сразу включает канал независимо от фазы прежнего WORK. */
LedIndicator_ProcessAt(&indicator, 2599U); CHECK(fake.outputs[0] == 1U); /* До истечения 100 мс светодиод продолжает гореть. */
LedIndicator_ProcessAt(&indicator, 2600U); CHECK(fake.outputs[0] == 0U); /* Ровно через 100 мс начинается оставшаяся пауза 900 мс. */
LedIndicator_ProcessAt(&indicator, 3499U); CHECK(fake.outputs[0] == 0U); /* Конец первого периода не порождает дополнительную вспышку. */
LedIndicator_ProcessAt(&indicator, 3500U); CHECK(fake.outputs[0] == 0U); /* Однократный ACTIVITY завершается выключенным уровнем на границе 1000 мс. */
LedIndicator_ProcessAt(&indicator, 4500U); CHECK(fake.outputs[0] == 0U); /* Без новых пакетов свободное периодическое мигание отсутствует. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 5000U) != 0U); /* Начинает новый приём после долгого отсутствия обмена. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 5050U) != 0U); /* Второй пакет приходит внутри текущей фазы свечения. */
LedIndicator_ProcessAt(&indicator, 5100U); CHECK(fake.outputs[0] == 1U); /* Старый срок выключения больше не действует: отсчёт перезапущен новым пакетом. */
LedIndicator_ProcessAt(&indicator, 5149U); CHECK(fake.outputs[0] == 1U); /* Новый пакет получает собственные полные 100 мс свечения. */
LedIndicator_ProcessAt(&indicator, 5150U); CHECK(fake.outputs[0] == 0U); /* Новая фаза паузы начинается ровно от последнего приёма. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, 5400U) != 0U && fake.outputs[0] == 1U); /* Приём во время паузы сразу включает LED вместо ожидания границы старого периода. */
LedIndicator_ProcessAt(&indicator, 5500U); CHECK(fake.outputs[0] == 0U); /* Повторный запуск в паузе не создаёт очередь пропущенных циклов. */
fake.now = 7000U; CHECK(LedIndicator_TriggerActivity(&indicator, 1U) != 0U && fake.outputs[1] == 1U); /* Проверяет вариант событийного API с часами аппаратного порта и другим каналом. */
fake.now = 7100U; LedIndicator_Process(&indicator); CHECK(fake.outputs[1] == 0U && fake.outputs[0] == 0U); /* Каналы не перезапускают друг другу фазы и не создают фоновую активность. */
CHECK(LedIndicator_TriggerActivityAt(&indicator, 0U, UINT32_MAX - 49U) != 0U); /* Начинает импульс непосредственно перед переполнением 32-битного времени. */
LedIndicator_ProcessAt(&indicator, 49U); CHECK(fake.outputs[0] == 1U); /* Через переполнение прошло 99 мс, поэтому свечение ещё активно. */
LedIndicator_ProcessAt(&indicator, 50U); CHECK(fake.outputs[0] == 0U); /* Через переполнение прошло ровно 100 мс, и LED гаснет без зависания. */
CHECK(LedIndicator_TriggerActivityAt(NULL, 0U, 0U) == 0U && LedIndicator_TriggerActivityAt(&indicator, 3U, 0U) == 0U); /* Ошибочный экземпляр и канал не вызывают запись чужого GPIO. */
puts("led indicator tests passed: receive 100/900 ms, immediate retrigger, no repeat, timer wrap"); /* Показывает прохождение новых событийных сценариев вместе с прежней диагностикой. */
return 0;
}
+23
View File
@@ -0,0 +1,23 @@
# Logic inputs in a native Scope UI
`include/logic_scope.h` is the portable, allocation-free C model for eight
logic inputs and PWM duty reconstruction. The FPGA register mapping and
framebuffer UI belong to the board application.
- `LogicScope`: bounded sample buffer, per-channel OFF/LOGIC/PWM mode,
sample clock/divider, capture origin, and retained snapshot.
- `logic_scope_plan`: chooses a count/divider for a requested duration;
limits are 65,536 samples and divider 65,535. Longer requested intervals
produce shorter captures; the UI must not stretch them to fill the view.
- `logic_scope_pwm`: emits midpoint/value pairs for complete active-high
PWM periods. Value is `2 * high_samples / period_samples - 1`. Initial
and final incomplete periods are omitted; constant levels have no PWM
period and emit no points. This is normalized modulation, not voltage.
The RK7020 consumer is `zynq_7020_OSC/demo/hdmi_usb_menu/`.
Its `tests/test_logic_scope.c` checks the capture plan, phase-shifted PWM,
constant inputs, menu selection, retained snapshots, and renderer bounds.
`include/scope_decoders.h` contains the portable DSView catalogue/configuration/annotation model; the isolated ARM adapter is in `tools/scope-decoders`. The framebuffer UI and UIO acquisition live in the Zynq board project.
`include/logic_stream.h` assembles contiguous stream blocks into bounded windows, checks block sequence, preserves trigger edges across block boundaries, and stops after one window in SINGLE. FREE/RISE/FALL/HIGH/LOW use an explicitly selected D0-D7 input. Frame publication is independent of how many windows the display renders. The board integration owns DMA acknowledgement, overflow handling and exclusive UIO access.
+63
View File
@@ -0,0 +1,63 @@
#ifndef SET_LOGIC_SCOPE_H
#define SET_LOGIC_SCOPE_H
#include <stdint.h>
#include <string.h>
#include <math.h>
#define LOGIC_SCOPE_MAX 65536u
typedef struct {
uint8_t samples[LOGIC_SCOPE_MAX];
unsigned mode[8]; /* 0 off, 1 logic, 2 PWM envelope */
unsigned count, clock, divider, running, single, generation;
unsigned trigger_mode, trigger_channel; /* FREE, RISE, FALL, HIGH, LOW */
unsigned stream, stream_rate; /* Separate acquisition mode; rate 0 follows timebase. */
uint64_t stream_samples;
double origin_s;
char status[96];
} LogicScope;
static inline unsigned logic_scope_channels(const LogicScope *s)
{
unsigned n=0;
for (unsigned i=0;i<8;++i) n+=s->mode[i]!=0;
return n;
}
/* Capture the requested interval with the finest representable sample period. */
static inline int logic_scope_plan(double span, unsigned clock, unsigned depth,
unsigned *count, unsigned *divider)
{
if (!isfinite(span) || span<=0 || !clock || depth<2 || depth>LOGIC_SCOPE_MAX) return 0;
double ticks=ceil(span*clock/depth);
ticks=fmax(1,fmin(65536,ticks));
*divider=(unsigned)ticks-1;
*count=(unsigned)fmax(2,fmin(depth,ceil(span*clock/ticks)));
return 1;
}
typedef void (*LogicScopePoint)(void *context, double time, double value);
/* Ignore clipped first/last cycles. A constant level has no measurable period. */
static inline unsigned logic_scope_pwm(const LogicScope *s, unsigned ch,
LogicScopePoint point, void *context)
{
if (ch>=8 || !s->clock || s->count<2) return 0;
unsigned rise=0,fall=0,have_rise=0,have_fall=0,n=0;
unsigned previous=(s->samples[0]>>ch)&1u;
double dt=(s->divider+1.0)/s->clock;
for (unsigned i=1;i<s->count;++i) {
unsigned level=(s->samples[i]>>ch)&1u;
if (level!=previous) {
if (level) {
if (have_rise && have_fall) {
point(context,s->origin_s+(rise+(i-rise)*0.5)*dt,
2.0*(fall-rise)/(i-rise)-1.0);
++n;
}
rise=i; have_rise=1; have_fall=0;
} else if (have_rise) { fall=i; have_fall=1; }
}
previous=level;
}
return n;
}
#endif
+37
View File
@@ -0,0 +1,37 @@
#ifndef SET_LOGIC_STREAM_H
#define SET_LOGIC_STREAM_H
#include "logic_scope.h"
typedef struct { uint8_t pending[LOGIC_SCOPE_MAX]; unsigned filled,target,previous,have_previous,triggered,sequence; } LogicStream;
static inline unsigned logic_stream_divider(double span,unsigned rate) {
static const unsigned fixed[]={0,1000,100,20,10};
if(rate>0&&rate<5)return fixed[rate];
double value=ceil(span*100000000.0/LOGIC_SCOPE_MAX);
return !isfinite(value)||value<10?10:value>65535?65535:(unsigned)value;
}
static inline void logic_stream_reset(LogicStream *state,unsigned target) {
memset(state,0,sizeof(*state));state->target=target<1?1:target>LOGIC_SCOPE_MAX?LOGIC_SCOPE_MAX:target;
}
static inline int logic_stream_match(unsigned mode,unsigned previous,unsigned current,unsigned valid,unsigned channel) {
unsigned mask=1u<<(channel&7),a=previous&mask,b=current&mask;
return !mode || (mode==1&&valid&&!a&&b)||(mode==2&&valid&&a&&!b)||(mode==3&&b)||(mode==4&&!b);
}
/* Sequence continuity is checked before touching the displayed frame. */
static inline int logic_stream_feed(LogicStream *state,LogicScope *scope,unsigned sequence,const uint8_t *bytes,unsigned count) {
if(sequence!=state->sequence)return -1;
++state->sequence;scope->stream_samples+=count;int updated=0;
unsigned i=0;
while(i<count){
while(!state->triggered&&i<count){unsigned current=bytes[i];
state->triggered=logic_stream_match(scope->trigger_mode,state->previous,current,state->have_previous,scope->trigger_channel);
if(!state->triggered){state->previous=current;state->have_previous=1;++i;}
}
if(i==count)break;
unsigned take=state->target-state->filled;if(take>count-i)take=count-i;
memcpy(state->pending+state->filled,bytes+i,take);state->filled+=take;i+=take;
state->previous=bytes[i-1];state->have_previous=1;
if(state->filled==state->target){memcpy(scope->samples,state->pending,state->target);scope->count=state->target;scope->origin_s=0;++scope->generation;updated=1;state->filled=0;state->triggered=0;
if(scope->single){scope->single=scope->running=0;break;}
}
}return updated;
}
#endif
+75
View File
@@ -0,0 +1,75 @@
#ifndef SCOPE_DECODERS_H
#define SCOPE_DECODERS_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define SD_ITEMS 256
#define SD_FIELDS 2048
#define SD_RESULTS 4096
typedef struct { char id[64],name[96],inputs[128],outputs[128]; int unavailable; } ScopeDecoder;
typedef struct { int owner,channel,mandatory; char id[64],name[128],value[128],choices[512]; } ScopeDecoderField;
typedef struct { uint64_t start,end; char decoder[64],row[96],text[256]; } ScopeAnnotation;
typedef struct {
ScopeDecoder items[SD_ITEMS]; ScopeDecoderField fields[SD_FIELDS]; ScopeAnnotation results[SD_RESULTS];
unsigned order[SD_ITEMS],count,field_count,result_count; int open,view,page,selected,edit,request,busy;
unsigned chain[8],chain_count,visible_fields[128],visible_count; double rate;
char status[160],edit_text[128];
} ScopeDecoders;
static inline void sd_copy(char *to,size_t size,const char *from) { size_t n=strlen(from); if(n>=size)n=size-1; memcpy(to,from,n);to[n]=0; }
static inline unsigned sd_split(char *line,char **out,unsigned cap) {
unsigned n=0; char *p=line; while(n<cap) { out[n++]=p; char *end=strchr(p,'\t'); if(!end)break;*end=0;p=end+1; }
if(n)out[n-1][strcspn(out[n-1],"\r\n")]=0; return n;
}
static inline int sd_find(const ScopeDecoders *s,const char *id) { for(unsigned i=0;i<s->count;++i)if(!strcmp(s->items[i].id,id))return (int)i;return -1; }
static inline int sd_has(const char *list,const char *token) {
size_t n=strlen(token);for(const char *p=list;*p;) {const char *e=strchr(p,'|');size_t k=e?(size_t)(e-p):strlen(p);if(k==n&&!strncmp(p,token,n))return 1;if(!e)break;p=e+1;}return 0;
}
static inline int sd_catalog(ScopeDecoders *s,const char *path) {
FILE *f=fopen(path,"r");if(!f)return 0;s->count=s->field_count=0;char line[4096];
while(fgets(line,sizeof(line),f)) {char *v[8];unsigned n=sd_split(line,v,8);
if((!strcmp(v[0],"D")||!strcmp(v[0],"E"))&&n>=3&&s->count<SD_ITEMS) {
ScopeDecoder *d=&s->items[s->count++];memset(d,0,sizeof(*d));sd_copy(d->id,sizeof(d->id),v[1]);sd_copy(d->name,sizeof(d->name),v[2]);d->unavailable=v[0][0]=='E';
if(n>=5){sd_copy(d->inputs,sizeof(d->inputs),v[3]);sd_copy(d->outputs,sizeof(d->outputs),v[4]);}
} else if((v[0][0]=='C'||v[0][0]=='O')&&n>=5&&s->field_count<SD_FIELDS) {
int owner=sd_find(s,v[1]);if(owner<0)continue;ScopeDecoderField *d=&s->fields[s->field_count++];memset(d,0,sizeof(*d));d->owner=owner;d->channel=v[0][0]=='C';
sd_copy(d->id,sizeof(d->id),v[2]);sd_copy(d->name,sizeof(d->name),v[3]);d->mandatory=d->channel&&atoi(v[4]);
sd_copy(d->value,sizeof(d->value),d->channel?"-1":n>5?v[5]:"");if(n>6)sd_copy(d->choices,sizeof(d->choices),v[6]);
}
}fclose(f);for(unsigned i=0;i<s->count;++i){unsigned j=i;s->order[i]=i;while(j>0&&strcmp(s->items[s->order[j-1]].id,s->items[i].id)>0){s->order[j]=s->order[j-1];--j;}s->order[j]=i;}snprintf(s->status,sizeof(s->status),"%u DSView decoders / D0-D7 inputs",s->count);return s->count!=0;
}
static inline int sd_resolve(ScopeDecoders *s,unsigned id,unsigned depth) {
if(depth>=8||id>=s->count||s->items[id].unavailable)return 0;
ScopeDecoder *d=&s->items[id];if(sd_has(d->inputs,"logic")){s->chain[s->chain_count++]=id;return 1;}
char inputs[128];sd_copy(inputs,sizeof(inputs),d->inputs);char *p=inputs;
while(*p) {char *end=strchr(p,'|');if(end)*end=0;int base=sd_find(s,p);
if(base>=0&&(unsigned)base!=id&&sd_resolve(s,(unsigned)base,depth+1)){s->chain[s->chain_count++]=id;return 1;}
for(int direct=1;direct>=0;--direct)for(unsigned i=0;i<s->count;++i)if(i!=id&&sd_has(s->items[i].inputs,"logic")==direct&&sd_has(s->items[i].outputs,p)&&sd_resolve(s,i,depth+1)){s->chain[s->chain_count++]=id;return 1;}
if(!end)break;p=end+1;
}return 0;
}
static inline void sd_select(ScopeDecoders *s,unsigned id) {
s->selected=(int)id;s->page=0;s->view=1;s->edit=-1;s->chain_count=s->visible_count=0;
if(!sd_resolve(s,id,0)){sd_copy(s->status,sizeof(s->status),"No supported logic input stack / import failed");return;}
unsigned next=0;
for(unsigned c=0;c<s->chain_count;++c)for(unsigned f=0;f<s->field_count;++f)if(s->fields[f].owner==(int)s->chain[c]&&s->visible_count<128){
s->visible_fields[s->visible_count++]=f;ScopeDecoderField *field=&s->fields[f];
if(field->channel&&field->mandatory&&atoi(field->value)<0&&next<8)snprintf(field->value,sizeof(field->value),"%u",next++);
}
sd_copy(s->status,sizeof(s->status),"Tap a field to change. DECODE uses the last digital snapshot.");
}
static inline int sd_config(const ScopeDecoders *s,const char *path) {
FILE *f=fopen(path,"w");if(!f)return 0;
for(unsigned c=0;c<s->chain_count;++c){unsigned id=s->chain[c];fprintf(f,"[%s]\n",s->items[id].id);
for(unsigned i=0;i<s->field_count;++i){const ScopeDecoderField *d=&s->fields[i];if(d->owner==(int)id)fprintf(f,"%s.%s=%s\n",d->channel?"ch":"opt",d->id,d->value);}}
int error=ferror(f);return fclose(f)==0&&!error;
}
static inline int sd_results(ScopeDecoders *s,const char *path) {
FILE *f=fopen(path,"r");if(!f)return 0;char line[2048];s->result_count=0;sd_copy(s->status,sizeof(s->status),"No annotations / check channel mapping and sample rate");
while(fgets(line,sizeof(line),f)){char *v[7];unsigned n=sd_split(line,v,7);
if(n>=6&&!strcmp(v[0],"A")&&s->result_count<SD_RESULTS){ScopeAnnotation *a=&s->results[s->result_count++];a->start=strtoull(v[1],NULL,10);a->end=strtoull(v[2],NULL,10);sd_copy(a->decoder,sizeof(a->decoder),v[3]);sd_copy(a->row,sizeof(a->row),v[4]);sd_copy(a->text,sizeof(a->text),v[5]);}
else if(n>=2&&!strcmp(v[0],"ERROR"))sd_copy(s->status,sizeof(s->status),v[1]);
else if(n>=3&&!strcmp(v[0],"DONE")&&s->result_count)snprintf(s->status,sizeof(s->status),"%u annotations / %s",s->result_count,v[2]);
}fclose(f);s->view=2;s->page=0;return 1;
}
#endif
+2
View File
@@ -1,5 +1,7 @@
# Работа с parallel NAND через GAS
> GAS `0x3` использует адреса отдельных 16-битных регистров. GAS SLOT `0x4` — отдельный сервис с маской в Word0 и тремя значениями Word1…Word3. Спецификация: `doc/setcan/protocan/OAP.md`; адреса этого документа остаются регистровыми.
## Архитектура
Внешняя NAND значительно больше общего адресного пространства GAS. Например,
+8
View File
@@ -0,0 +1,8 @@
# Результаты отдельных конфигураций CCS воспроизводятся установленным TI compiler.
/projects/ccs/*/Debug/
/projects/ccs/*/Release/
# Служебные локальные настройки IDE и launch не входят в переносимый проект.
/projects/ccs/*/.settings/
/projects/ccs/*/.launches/
# Python cache не является исходником общего модуля или проекта.
**/__pycache__/

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