Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a197437985 | ||
|
|
ae018a1347 | ||
|
|
80d8fe1794 | ||
|
|
d057df7698 | ||
|
|
f9d369d032 | ||
|
|
7b1a16d6e4 | ||
|
|
a640f98f74 | ||
|
|
f0edb70dbe | ||
|
|
6d59c3812c | ||
|
|
e144c3e86a | ||
|
|
fa2ab3cc65 | ||
|
|
5d2831794d | ||
|
|
a1b2835457 | ||
|
|
dc4b0ab71b | ||
|
|
cf75c5c855 | ||
|
|
491fa11569 | ||
|
|
149d19f3f9 | ||
|
|
45a34db657 | ||
|
|
bf5f094249 | ||
|
|
41e24629ce | ||
|
|
e52ba797e4 | ||
|
|
83fb9b6fe8 | ||
|
|
41f5b879fc | ||
|
|
f17c124f0e | ||
|
|
2da7e7b525 | ||
|
|
62c0492bfd | ||
|
|
5bf3167064 | ||
|
|
c6390ae1c9 | ||
|
|
876b58517c | ||
|
|
6ecf057a86 | ||
|
|
d42f2e1622 | ||
|
|
0c4eab4615 | ||
|
|
5af8a8e9c8 | ||
|
|
f613101124 | ||
|
|
cf65439400 | ||
|
|
a5e46d4627 | ||
|
|
95c8b8ac7d | ||
|
|
32e2fbedc0 | ||
|
|
f6d45b70d4 | ||
|
|
226c2ca599 | ||
|
|
f2956b54cd | ||
|
|
eab8dc5e0a | ||
|
|
131f306131 | ||
|
|
4fc0ad4e14 | ||
|
|
da055c2787 | ||
|
|
8fc2e1b5cf | ||
|
|
b5b2d1ae37 | ||
|
|
7dfa8bd94d |
@@ -4,3 +4,8 @@
|
||||
|
||||
*.pdf binary
|
||||
*.png binary
|
||||
|
||||
# CMD.exe требует CRLF в BAT, иначе после переноса Git часть команд разбирается неверно.
|
||||
*.bat text eol=crlf
|
||||
# Командные файлы CMD выполняются тем же интерпретатором Windows и требуют CRLF.
|
||||
*.cmd text eol=crlf
|
||||
|
||||
@@ -27,3 +27,6 @@ Desktop.ini
|
||||
|
||||
# Generated native library (built before packaging)
|
||||
python/protocan/native/
|
||||
|
||||
# Кэш pytest создаётся проверками автоматически и не входит в исходники общей библиотеки.
|
||||
.pytest_cache/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "third_party/kdiff3"]
|
||||
path = third_party/kdiff3
|
||||
url = https://invent.kde.org/sdk/kdiff3.git
|
||||
+1
-1
@@ -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 и конкретной разводки адаптера; переносимая логика протоколов
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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-контроллера. */
|
||||
} /* Завершение управления встроенным окончанием шины. */
|
||||
|
||||
@@ -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 — включён. */
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Загрузчик F28335 по RS для periph_28335
|
||||
|
||||
Постоянный загрузчик принимает `INITLOAD` (58), `LOAD` (51) и `TFLASH` (55),
|
||||
совместимые с Flash RS в терминале `periph_28335` SETGUI. Ядро не зависит от
|
||||
UART, GPIO, ОС и Qt. Оно использует существующий валидатор и writer
|
||||
`c/protocan-boot/ports/tms320f28335`, сохраняя единый формат SCI8/FAB1.
|
||||
|
||||
Слои: клиент periph_28335 → автомат RS → проверка SCI8/FAB1 → callbacks платы
|
||||
→ TI Flash API. Передаются октеты; адреса CPU считаются 16-битными словами.
|
||||
|
||||
| Файл | Назначение и зависимости |
|
||||
|---|---|
|
||||
| `include/f28335_rs_boot.h`, `src/f28335_rs_boot.c` | Переносимый автомат, зависит от protocan-boot |
|
||||
| `ports/tms320f28335/flash_api.c` | Официальный TI API v2.10, заголовки устройства, частота F28335_RS_CPU_HZ |
|
||||
| `ports/tms320f28335/boot.cmd` | Постоянный сектор A, отдельные RAMAPI, RAMFUNCS и стек |
|
||||
| `tools/build.py` | Сборка boot и выпуск проверенных Flash OUT/SCI8 с manifest/SHA-256 |
|
||||
| `tests/test_boot.py` | Настоящий автомат и writer в модели RAM/Flash без оборудования |
|
||||
|
||||
Приложение занимает `[0x300000,0x338000)`, первые 16 слов содержат FAB1.
|
||||
Сектор A и переход `0x33FFF6` принадлежат постоянному загрузчику.
|
||||
`INITLOAD` разрешает только staging `0x200000`, чётный размер 24..130560
|
||||
октетов. Ошибочные CRC, таймаут и ошибка UART отзывают подтверждение LOAD.
|
||||
Перед erase повторно проверяются RAM, CRC32, все адреса, entry и FAB1.
|
||||
Общий writer пишет FAB1 последним и проверяет физическую Flash.
|
||||
Ошибка не удаляет recovery; перезапуск разрешён после успешного ACK.
|
||||
|
||||
Контракт callbacks: `stage_write(user, offset, octet)` и `stage.read(user,
|
||||
offset, buffer, size)` обслуживают RAM относительно начала файла;
|
||||
`transmit(user, data, size)` возвращает true после физического окончания TX;
|
||||
`program(user, image)` вызывает общий Flash writer; `reboot(user)` сбрасывает
|
||||
контроллер. Все чтения/записи должны завершиться полностью либо вернуть false.
|
||||
Прерывания в постоянном загрузчике запрещены, watchdog выключен на время
|
||||
стирания. Межбайтовый таймаут аппаратный порт передаёт в `f28335_rs_abort`.
|
||||
|
||||
Быстрый старт: выделить `f28335_rs_boot_t` в RAM, заполнить пять callbacks,
|
||||
вызвать `f28335_rs_init(&boot, &port, 16)` и передавать каждый октет SCI
|
||||
через `f28335_rs_feed(&boot, octet)`. До запуска приложения вызвать
|
||||
`f28335_flash_app_validate`, после успеха перейти на возвращённый entry.
|
||||
Аппаратные таймауты, выключение силовых выходов и выбор скорости задаёт плата.
|
||||
|
||||
Подключено в `prolitarski_ups`: SCI-A/RS-422, 115200 8N1, адрес 16;
|
||||
Monitor №93=1 в остановленном состоянии запрашивает boot через watchdog.
|
||||
Первичная установка требует записи двух OUT через JTAG с сохранением
|
||||
сектора A при загрузке приложения. Все последующие RS-обновления меняют B–H.
|
||||
Повреждённое приложение оставляет устройство в RS recovery после сброса.
|
||||
@@ -0,0 +1,23 @@
|
||||
#ifndef F28335_RS_BOOT_H /* Один контракт обслуживает постоянный загрузчик и его модель на ПК. */
|
||||
#define F28335_RS_BOOT_H /* Исключает повторные объявления при подключении аппаратного порта. */
|
||||
#include "../../protocan-boot/ports/tms320f28335/f28335_flash_update.h" /* Повторно использует проверку SCI8/FAB1 и запись с защитой сектора A. */
|
||||
#define F28335_RS_STAGE 0x200000UL /* Адрес слова внешней RAM совпадает с INITLOAD в periph_28335. */
|
||||
#define F28335_RS_LIMIT F28335_AB_SLOT_BYTES /* Проверяемый SCI8 ограничен 130560 октетами общего валидатора F28335. */
|
||||
typedef struct { /* Аппаратная плата владеет памятью и UART; ядро владеет только состоянием протокола. */
|
||||
bool (*stage_write)(void *, uint32_t, unsigned int); /* Записывает один октет по относительному смещению staging RAM. */
|
||||
f28335_boot_io_t stage; /* Читает октеты staging для повторной CRC32 и проверки всех адресов перед erase. */
|
||||
bool (*transmit)(void *, const unsigned int *, unsigned int); /* Передаёт полный ответ и дожидается последнего стопового бита. */
|
||||
unsigned int (*program)(void *, f28335_flash_update_t *); /* Возвращает 0 после записи и проверки, код TI либо 1004/1006 при защите или ошибке проверки; управление остаётся в секторе A. */
|
||||
void (*reboot)(void *); /* Выполняет reset только после успешного ответа о завершении записи. */
|
||||
void *user; /* Независимый контекст UART, Flash и модели испытаний. */
|
||||
} f28335_rs_port_t; /* Аппаратный порт не собирает кадры и не дублирует алгоритм проверки образа. */
|
||||
typedef struct { /* Экземпляр допускает работу без heap и без прерываний. */
|
||||
f28335_rs_port_t port; /* Сохраняет callbacks на весь сеанс обновления. */
|
||||
f28335_flash_update_t image; /* Ограниченный индекс адресных секций и страничный кеш общего валидатора. */
|
||||
uint32_t length, position, expected, received, crc32; /* Длины и позиции считаются октетами, независимо от sizeof(char) C28x. */
|
||||
unsigned int address, command, crc16, wire_crc, loaded, fault, header[16]; /* Заголовок вмещает максимальный фиксированный запрос TFLASH на 16 октетов. */
|
||||
} f28335_rs_boot_t; /* Загруженный блок никогда не исполняется как произвольный код RAM. */
|
||||
void f28335_rs_init(f28335_rs_boot_t *s, const f28335_rs_port_t *port, unsigned int address); /* Настраивает адрес 1..255 и сбрасывает разрешение программирования. */
|
||||
void f28335_rs_abort(f28335_rs_boot_t *s); /* Сбрасывает неполный кадр и подтверждение LOAD при таймауте или аппаратной ошибке SCI. */
|
||||
void f28335_rs_feed(f28335_rs_boot_t *s, unsigned int value); /* Принимает один восьмибитный октет совместимых INITLOAD/LOAD/TFLASH. */
|
||||
#endif /* Завершает публичный интерфейс общего RS-загрузчика. */
|
||||
@@ -0,0 +1,28 @@
|
||||
MEMORY /* Все адреса и размеры относятся к 16-битным словам CPU C28x. */
|
||||
{ /* Постоянный загрузчик использует только сектор A и внутреннюю RAM. */
|
||||
PAGE 0: /* Исполняемый код и адреса загрузки из Flash. */
|
||||
FLASHA : origin = 0x338000, length = 0x007F80 /* Область постоянного boot заканчивается до служебной области CSM. */
|
||||
BEGIN : origin = 0x33FFF6, length = 0x000002 /* Два слова аппаратного перехода Boot ROM в режиме Flash. */
|
||||
RAMAPI : origin = 0x008000, length = 0x001800 /* 6144 слова предназначены для копии официального TI Flash API. */
|
||||
RAMFUNCS : origin = 0x00F800, length = 0x000800 /* InitFlash выполняется из RAM, не затрагивая код самого API. */
|
||||
RESET : origin = 0x3FFFC0, length = 0x000002 /* Вектор reset находится в ROM и не программируется. */
|
||||
PAGE 1: /* Данные загрузчика не зависят от размещения будущего приложения. */
|
||||
RAMM0 : origin = 0x000058, length = 0x0003A8 /* Слова 50..57 сохранены для запроса boot через watchdog reset. */
|
||||
RAMM1 : origin = 0x000400, length = 0x000400 /* Стек на 1024 слова вмещает буфер writer и контекст runtime. */
|
||||
RAMDATA : origin = 0x00E000, length = 0x001800 /* Индекс SCI8, состояния протокола и аппаратные данные находятся в RAM. */
|
||||
} /* Внешняя RAM используется через callbacks и не содержит исполняемого кода. */
|
||||
SECTIONS /* C runtime загрузчика, в отличие от приложения, целиком сохраняется в A. */
|
||||
{ /* Размещение API указано до общей .text, чтобы извлечь все его код и константы. */
|
||||
codestart : > BEGIN, PAGE = 0 /* Штатный TI startup запрещает watchdog и вызывает _c_int00 загрузчика. */
|
||||
flash_api : { -lFlash28335_API_V210.lib(.text) -lFlash28335_API_V210.lib(.econst) } LOAD = FLASHA, RUN = RAMAPI, LOAD_START(_FlashApiLoadStart), LOAD_END(_FlashApiLoadEnd), RUN_START(_FlashApiRunStart), PAGE = 0 /* Ни одна функция TI API не исполняется из занятого банка Flash. */
|
||||
.text : > FLASHA, PAGE = 0 /* Протокол, валидатор и собственный runtime загрузчика остаются после обновления приложения. */
|
||||
.cinit : > FLASHA, PAGE = 0 /* Инициализация данных не зависит от целостности B–H. */
|
||||
.pinit : > FLASHA, PAGE = 0 /* Поддерживает стандартную секцию инициализации COFF. */
|
||||
.econst : > FLASHA, PAGE = 0 /* Таблицы и константы загрузчика также защищены от erase B–H. */
|
||||
.switch : > FLASHA, PAGE = 0 /* Таблицы переходов автомата не находятся в заменяемом приложении. */
|
||||
ramfuncs : LOAD = FLASHA, RUN = RAMFUNCS, LOAD_START(_RamfuncsLoadStart), LOAD_END(_RamfuncsLoadEnd), RUN_START(_RamfuncsRunStart), PAGE = 0 /* InitFlash переносится до изменения Flash wait states. */
|
||||
.stack : > RAMM1, PAGE = 1 /* Стек отделён от handoff и буфера внешней RAM. */
|
||||
.ebss : > RAMDATA, PAGE = 1 /* Нулевая инициализация касается только состояния boot. */
|
||||
.esysmem : > RAMDATA, PAGE = 1 /* Оставляет корректную секцию runtime без использования heap в алгоритме. */
|
||||
.reset : > RESET, PAGE = 0, TYPE = DSECT /* ROM-вектор не попадает в программируемый OUT. */
|
||||
} /* Пароли CSM, OTP и сектора приложения не создаются в образе загрузчика. */
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "DSP2833x_Device.h" /* Использует официальные описания регистров выбранной платы F28335. */
|
||||
#include "../../include/f28335_rs_boot.h" /* Получает общий writer с повторной проверкой staging перед erase. */
|
||||
#include "Flash2833x_API_Library.h" /* Официальный TI API v2.10 выполняет физические операции внутренней Flash. */
|
||||
#include <string.h> /* Копирует код и таблицы TI API в RAM до изменения общего банка Flash. */
|
||||
extern Uint16 FlashApiLoadStart, FlashApiLoadEnd, FlashApiRunStart; /* LOAD/RUN-символы создаёт linker постоянного загрузчика. */
|
||||
static FLASH_ST api_status; /* Диагностика TI хранится в RAM и доступна во время занятости Flash. */
|
||||
static unsigned int api_error; /* Код последней операции TI передаётся клиенту без подмены успехом. */
|
||||
static bool erase(void *user) /* Стирает исключительно семь секторов приложения B–H. */
|
||||
{ /* Сам API исполняется из RAM; вызывающий код продолжит работу из сохранённого сектора A. */
|
||||
(void)user; api_error = Flash_Erase(0x00FEU, &api_status); return api_error == STATUS_SUCCESS; /* В TI v2.10 бит 0 выбирает A, маска FE исключает его и сохраняет recovery; результат сохраняет диагностику API. */
|
||||
} /* Ненулевой результат запрещает программирование данных. */
|
||||
static bool program(void *user, uint32_t address, uint16_t *data, unsigned int words) /* Записывает и немедленно проверяет одну ограниченную порцию. */
|
||||
{ /* Дополнительная аппаратная проверка диапазона защищает boot независимо от wire-парсера. */
|
||||
Uint16 result; (void)user; /* Результат официального API не подменяется успешным кодом протокола. */
|
||||
if (!words || address < F28335_FLASH_APP_START || address >= F28335_FLASH_APP_STOP || words > F28335_FLASH_APP_STOP - address) return false; /* Исключает сектор A, CSM, OTP и переполнение верхней границы. */
|
||||
result = Flash_Program((Uint16 *)address, data, words, &api_status); /* Данные и рабочий буфер находятся в RAM. */
|
||||
if (!result) result = Flash_Verify((Uint16 *)address, data, words, &api_status); /* Подтверждает физическую запись до перехода к следующей порции. */
|
||||
api_error = result; return result == STATUS_SUCCESS; /* Ошибка оставляет FAB1 неопубликованным, сохраняет код TI и RS recovery. */
|
||||
} /* Все прерывания остаются запрещёнными на протяжении жизни постоянного загрузчика. */
|
||||
uint16_t F28335Rs_ReadFlash(void *user, uint32_t address) /* Читает физическое слово для общей проверки приложения перед запуском. */
|
||||
{ /* Общий валидатор ограничивает адреса диапазоном B–H. */
|
||||
(void)user; return *(volatile Uint16 *)address; /* Чтение применяется только вне активной операции Flash API. */
|
||||
} /* Стёртые промежутки проверяются вместе с загруженными секциями. */
|
||||
unsigned int F28335Rs_Program(void *user, f28335_flash_update_t *image) /* Callback работает из сектора A и возвращает полный статус, совместимый с Flash RS periph_28335. */
|
||||
{ /* Проект задаёт реальный SYSCLK в герцах через F28335_RS_CPU_HZ при компиляции. */
|
||||
f28335_flash_ops_t ops; (void)user; /* Таблица операций не содержит обращений к приложению из B–H. */
|
||||
if (CsmRegs.CSMSCR.bit.SECURE) return 0x1004U; /* Не начинает стирание защищённого CSM устройства и передаёт клиенту стандартный статус защиты. */
|
||||
memcpy(&FlashApiRunStart, &FlashApiLoadStart, &FlashApiLoadEnd - &FlashApiLoadStart); /* Все .text и .econst TI API копируются во внутреннюю RAM. */
|
||||
Flash_CPUScaleFactor = (Uint32)(1048576.0L * 200.0L * F28335_RS_CPU_HZ / 1000000000.0L); /* Формула IQ20 из TI v2.10 учитывает фактическую частоту CPU, единицы — Гц. */
|
||||
Flash_CallbackPtr = 0; ops.erase = erase; ops.program = program; ops.read = F28335Rs_ReadFlash; ops.user = 0; /* Callback во Flash во время erase/program запрещён. */
|
||||
api_error = 0U; if (f28335_flash_update_program(image, &ops)) return 0U; /* Повторная CRC staging, запись FAB1 последним и CRC физической Flash находятся в общем writer; ноль означает подтверждённый успех. */
|
||||
return api_error ? api_error : 0x1006U; /* Ошибка TI сохраняет свой код, отказ структуры/readback без ошибки API получает отдельный ненулевой статус. */
|
||||
} /* После отказа управление остаётся в постоянном загрузчике и допускает новый LOAD. */
|
||||
@@ -0,0 +1,79 @@
|
||||
#include "../include/f28335_rs_boot.h" /* Единственная реализация автомата используется платой и host-тестами. */
|
||||
#include <string.h> /* Сбрасывает состояние без динамического выделения памяти. */
|
||||
static unsigned int crc16(unsigned int crc, unsigned int value) /* CRC16 Modbus защищает запросы и ответы существующего periph_28335. */
|
||||
{ /* Все операции явно ограничены 16 битами и подходят для 16-битного int C28x. */
|
||||
unsigned int bit; /* Каждый принятый октет содержит ровно восемь значащих битов. */
|
||||
crc ^= value & 255U; /* Старшие биты аппаратного регистра SCI не являются данными протокола. */
|
||||
for (bit = 0U; bit < 8U; ++bit) crc = (crc & 1U) ? ((crc >> 1) ^ 0xA001U) : (crc >> 1); /* Отражённый полином A001 соответствует CRC16 Modbus. */
|
||||
return crc & 65535U; /* На ПК unsigned int шире C28x, поэтому маска сохраняет одинаковый результат. */
|
||||
} /* Завершает переносимый шаг CRC. */
|
||||
static uint32_t get32(const unsigned int *p) /* Поля BIOS передаются младшим октетом вперёд, в отличие от адреса внутри SCI8. */
|
||||
{ /* Приведение перед сдвигом исключает потерю старших битов на 16-битном int. */
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); /* Возвращает адрес слова либо длину октетов по назначению поля. */
|
||||
} /* Формат запроса совпадает с periph28335_firmware.request. */
|
||||
static bool reply(f28335_rs_boot_t *s, unsigned int command, unsigned int status) /* TFLASH сообщает конкретный статус, остальные успешные команды — короткий ACK. */
|
||||
{ /* Ответ не содержит хвостового паддинга, который клиент periph_28335 считает необязательным. */
|
||||
unsigned int frame[6], crc = 0xFFFFU, i, size = command == 55U ? 4U : 2U; /* CMD_TFLASH=55 включает два октета результата; ACK INITLOAD/LOAD содержит адрес и команду. */
|
||||
frame[0] = s->address; frame[1] = command; frame[2] = status & 255U; frame[3] = status >> 8; /* Сохраняет принятый адрес и little-endian статус. */
|
||||
for (i = 0U; i < size; ++i) crc = crc16(crc, frame[i]); /* Контрольная сумма защищает все значащие поля ответа. */
|
||||
frame[size] = crc & 255U; frame[size + 1U] = crc >> 8; /* CRC16 передаётся младшим октетом вперёд. */
|
||||
return s->port.transmit(s->port.user, frame, size + 2U); /* Reset разрешается только после фактического завершения передачи. */
|
||||
} /* Ошибка UART никогда не превращается в разрешение перезапуска. */
|
||||
void f28335_rs_abort(f28335_rs_boot_t *s) /* Инвалидирует частичный кадр и прежнее подтверждение образа. */
|
||||
{ /* Порт вызывает эту функцию при межбайтовом таймауте, переполнении либо RXERROR. */
|
||||
s->position = 0UL; s->loaded = 0U; s->length = 0UL; s->image.valid = false; /* Новый TFLASH после сбоя требует полного нового INITLOAD/LOAD. */
|
||||
} /* Прерывание сеанса не изменяет действующую Flash. */
|
||||
void f28335_rs_init(f28335_rs_boot_t *s, const f28335_rs_port_t *port, unsigned int address) /* Создаёт независимый экземпляр без скрытых глобальных переменных. */
|
||||
{ /* Конфигурация платы должна передать все обязательные callbacks. */
|
||||
memset(s, 0, sizeof(*s)); s->port = *port; s->address = address; /* Обнуляет кеш, счётчики и разрешение LOAD до первого принятого кадра. */
|
||||
} /* Аппаратная инициализация SCI и GPIO остаётся в проекте платы. */
|
||||
static void execute(f28335_rs_boot_t *s) /* Выполняет только полностью принятый запрос с подтверждённой CRC16. */
|
||||
{ /* Транспортные поля никогда не разыменовываются как произвольные CPU-адреса. */
|
||||
uint32_t source, destination, length; unsigned int status = 0U; pcan_boot_manifest_t manifest; /* Manifest общего валидатора используется только для размера и транспортной CRC32. */
|
||||
if (s->command == 58U) { /* CMD_INITLOAD=58 резервирует фиксированный безопасный staging вместо произвольной записи памяти. */
|
||||
source = get32(s->header + 2); length = get32(s->header + 6); s->loaded = 0U; s->length = 0UL; /* Новая подготовка всегда отзывает подтверждение прежнего блока. */
|
||||
if (source == F28335_RS_STAGE && length >= 24UL && length <= F28335_RS_LIMIT && !(length & 1UL)) { s->length = length; reply(s, 58U, 0U); } /* Чётный SCI8 должен поместиться в предел общего проверяемого формата. */
|
||||
} else if (s->command == 51U) { /* CMD_LOAD=51 уже принял поток в staging, но до этой точки не разрешал запись Flash. */
|
||||
if (!s->fault && s->length) { s->loaded = 1U; reply(s, 51U, 0U); } /* Подтверждает блок только после полного приёма, успешной записи RAM и CRC16. */
|
||||
} else if (s->command == 55U) { /* CMD_TFLASH=55 совместим с шестибайтовым статусом periph_28335. */
|
||||
destination = get32(s->header + 2); source = get32(s->header + 6); length = get32(s->header + 10); /* Назначение, источник и размер сохраняют порядок полей существующего GUI. */
|
||||
if (destination || source || length) { /* Нулевая тройка служит безопасной пробой поддержки без стирания и запуска. */
|
||||
if (!s->loaded || destination != F28335_FLASH_APP_START || source != F28335_RS_STAGE || length != s->length) status = 0x1003U; /* Требует ровно тот блок, который подтвердил завершённый LOAD. */
|
||||
else { /* Общий валидатор повторно читает RAM и сравнивает CRC32, записанную при приёме. */
|
||||
memset(&manifest, 0, sizeof(manifest)); manifest.image_size = length; manifest.image_crc32 = s->crc32; /* Не доверяет CRC из непроверенного пользовательского manifest. */
|
||||
if (!f28335_flash_update_validate(&s->image, &s->port.stage, 0UL, &manifest)) status = 0x1002U; /* Отвергает сектор A, CSM, OTP, перекрытия, неверный FAB1 и повреждённые данные до erase. */
|
||||
else status = s->port.program(s->port.user, &s->image); /* Сохраняет конкретный код TI, защиту CSM 1004 и ошибку readback 1006; отказ оставляет RS recovery доступным. */
|
||||
} /* Завершает проверку и аппаратную запись текущего образа. */
|
||||
s->loaded = 0U; /* Повторная команда записи требует нового подтверждённого LOAD независимо от результата. */
|
||||
} /* Безопасная проба не потребляет подтверждённый блок RAM. */
|
||||
if (reply(s, 55U, status) && !status && length) s->port.reboot(s->port.user); /* Успешный reset следует только после записи, проверки физической Flash и отправки ACK. */
|
||||
} /* Остальные команды не поддерживаются и не меняют память. */
|
||||
} /* EEPROM параметров не входит ни в один callback загрузчика. */
|
||||
void f28335_rs_feed(f28335_rs_boot_t *s, unsigned int value) /* Разбирает запросы последовательно без большого приёмного массива. */
|
||||
{ /* Один вызов обрабатывает ровно один октет; время межбайтового ожидания контролирует порт. */
|
||||
uint32_t position = s->position; pcan_boot_byte_t octet = value & 255U; /* Маска исключает флаги SCI и старшие биты широкого char. */
|
||||
if (!position) { /* Новый кадр начинается только с адреса этого устройства. */
|
||||
if (octet != s->address || !s->address || s->address > 255U) return; /* Чужой адрес не получает ACK и не запускает запись RAM. */
|
||||
s->crc16 = 0xFFFFU; s->wire_crc = 0U; s->fault = 0U; /* CRC16 каждого запроса начинается со значения FFFF протокола Modbus. */
|
||||
} /* После первого октета автомат ожидает номер команды. */
|
||||
if (position == 1UL) { /* Второй октет задаёт фиксированную либо потоковую длину кадра. */
|
||||
s->command = octet; /* Сохраняет неизменный номер BIOS-команды. */
|
||||
if (octet == 58U) s->expected = 12UL; /* INITLOAD содержит адрес/команду, два u32 и CRC16: 12 октетов. */
|
||||
else if (octet == 55U) s->expected = 16UL; /* TFLASH содержит три u32 и занимает 16 октетов с CRC16. */
|
||||
else if (octet == 51U && s->length) { s->expected = s->length + 4UL; s->loaded = 0U; s->crc32 = 0UL; } /* LOAD состоит из адреса/команды, точного SCI8 и CRC16. */
|
||||
else { f28335_rs_abort(s); return; } /* Неизвестная команда не позволяет использовать оставшиеся байты как адрес CPU. */
|
||||
} /* Только распознанный заголовок разрешает приём полезных данных. */
|
||||
if (position < 2UL || s->command != 51U) { if (position < 16UL) s->header[position] = octet; } /* Фиксированные запросы сохраняются в ограниченном заголовке. */
|
||||
else if (position < s->expected - 2UL) { /* Поток LOAD записывается в RAM, пока завершающая CRC16 ещё не подтверждена. */
|
||||
if (!s->port.stage_write(s->port.user, position - 2UL, octet)) s->fault = 1U; /* Ошибка RAM запрещает подтверждение и последующий erase. */
|
||||
s->crc32 = pcan_boot_crc32_update(s->crc32, &octet, 1U); /* Независимая CRC32 позволяет обнаружить изменение staging после LOAD. */
|
||||
} /* Два завершающих октета CRC не попадают в staging образа. */
|
||||
if (position < 2UL || position < s->expected - 2UL) s->crc16 = crc16(s->crc16, octet); /* CRC16 включает адрес, команду и всю полезную нагрузку. */
|
||||
else if (position == s->expected - 2UL) s->wire_crc = octet; /* Первый октет контрольной суммы является младшим. */
|
||||
else s->wire_crc |= (unsigned int)octet << 8; /* Второй октет завершает 16-битную CRC принятого кадра. */
|
||||
s->position = position + 1UL; /* Продвигает курсор только после обработки текущего октета. */
|
||||
if (position >= 1UL && s->position == s->expected) { /* Запрос исполняется ровно один раз после получения последнего октета. */
|
||||
s->position = 0UL; /* Следующий адрес начнёт независимый кадр даже при ошибочной CRC текущего. */
|
||||
if (s->wire_crc == s->crc16) execute(s); else f28335_rs_abort(s); /* Повреждённый кадр отзывает прежний LOAD и никогда не вызывает program. */
|
||||
} /* Частичный кадр остаётся в ожидании следующего октета либо внешнего timeout. */
|
||||
} /* Автомат не использует системные часы, регистры MCU или динамическую память. */
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,88 @@
|
||||
"""Проверяет production-автомат RS и общий writer на модели сбоев без платы.""" # Не вызывает JTAG, COM и TI Flash API.
|
||||
import ctypes # Передаёт октеты настоящему скомпилированному C-автомату.
|
||||
import importlib.util # Использует общий подписыватель FAB1 для корректных тестовых образов.
|
||||
import os # Добавляет runtime GCC только в окружение тестового процесса.
|
||||
from pathlib import Path # Разрешает пути относительно установленной библиотеки templates.
|
||||
import struct # Формирует стандартные поля запросов и адресные секции SCI8.
|
||||
import subprocess # Собирает C-модель с production-исходниками GCC.
|
||||
import tempfile # Удаляет только собственные временные DLL проверки.
|
||||
import unittest # Каждый отказ выполняется независимо и получает отдельный результат.
|
||||
LIBRARY = Path(__file__).resolve().parents[1]; TEMPLATES = LIBRARY.parents[1]; PORT = TEMPLATES / "c/protocan-boot/ports/tms320f28335" # Тест использует те же валидатор и writer, которые компилируются для платы.
|
||||
spec = importlib.util.spec_from_file_location("boot_test_contract", TEMPLATES / "python/protocan/f28335_flash.py"); contract = importlib.util.module_from_spec(spec); spec.loader.exec_module(contract) # Эталон FAB1 берётся из действующей общей библиотеки.
|
||||
|
||||
def section(address, words): # Создаёт адресный блок TI без дополнительных правил обновления.
|
||||
return struct.pack("<HHH", len(words), address >> 16, address & 65535) + struct.pack("<" + "H" * len(words), *words) # Длина считается словами, каждое слово little endian, адрес — старшим словом вперёд.
|
||||
|
||||
def image(): # Минимальный правильный образ содержит разрыв для проверки CRC стёртых промежутков.
|
||||
entry = contract.APP_DATA; loaded = {entry: 0x1234, entry + 1: 0x5678, entry + 12: 0xAABB, entry + 13: 0xCCDD} # Промежуток между двумя секциями должен остаться FFFF после erase.
|
||||
return b"\xaa\x08" + bytes(16) + struct.pack("<HH", entry >> 16, entry & 65535) + section(contract.APP_START, struct.unpack("<16H", contract.descriptor(entry, loaded))) + section(entry, [0x1234, 0x5678]) + section(entry + 12, [0xAABB, 0xCCDD]) + bytes(2) # Заголовок FAB1 передаётся первым, но writer обязан опубликовать его последним.
|
||||
|
||||
class BootTests(unittest.TestCase): # Проверяет наблюдаемое поведение полного обмена, а не отдельные строки реализации.
|
||||
@classmethod # DLL компилируется один раз для всех сценариев отказа.
|
||||
def setUpClass(cls): # Изолирует бинарный результат и зависимости GCC.
|
||||
gcc = Path(os.environ.get("CC", "C:/msys64/mingw64/bin/gcc.exe")); os.environ["PATH"] = str(gcc.parent) + os.pathsep + os.environ["PATH"] # CC позволяет выбрать другой совместимый GCC без изменения теста.
|
||||
cls.temporary = tempfile.TemporaryDirectory(prefix="f28335-rs-tests-"); dll = Path(cls.temporary.name) / "model.dll" # Все артефакты испытания находятся вне прошивки пользователя.
|
||||
subprocess.run([str(gcc), "-shared", "-std=c99", "-Wall", "-Wextra", "-Werror", str(LIBRARY / "tests/model.c"), str(PORT / "f28335_flash_app.c"), str(PORT / "f28335_flash_update.c"), str(TEMPLATES / "c/protocan-boot/src/pcan_boot.c"), "-o", str(dll)], check=True) # Компилирует production parser, CRC, validator и writer без извлечения копий функций.
|
||||
cls.native = ctypes.CDLL(str(dll)); cls.native.model_crc.argtypes = [ctypes.c_char_p, ctypes.c_uint]; cls.native.model_crc.restype = ctypes.c_uint; cls.native.model_value.restype = ctypes.c_uint # ABI содержит только явно заданные целые числа и октеты.
|
||||
|
||||
@classmethod # Windows требует освободить библиотеку до удаления DLL.
|
||||
def tearDownClass(cls): # Закрывает только дескриптор, созданный этим тестом.
|
||||
if os.name == "nt": # На Windows открытая DLL удерживает файл от удаления.
|
||||
release = ctypes.WinDLL("kernel32").FreeLibrary; release.argtypes = [ctypes.c_void_p]; release.restype = ctypes.c_int; release(cls.native._handle) # Освобождает единственную тестовую DLL с полным размером указателя.
|
||||
cls.temporary.cleanup() # Временные файлы не остаются в библиотеке или проекте ИБП.
|
||||
|
||||
def setUp(self): self.native.model_reset() # Стирает модель между тестами, сохраняя независимость сценариев.
|
||||
|
||||
def frame(self, command, payload=b"", address=16): # Формирует запросы совместимого BIOS для управления production-автоматом.
|
||||
body = bytes((address, command)) + payload; return body + struct.pack("<H", self.native.model_crc(body, len(body))) # Повреждение CRC тестируется отдельно, а фиксированный probe-вектор проверяет численное значение.
|
||||
|
||||
def send(self, data): # Аппаратные границы чтений могут разделить кадр после любого октета.
|
||||
for octet in data: self.native.model_feed(octet) # Никакая модель не собирает кадр за production-автомат.
|
||||
return bytes(self.native.model_value(10 + i) for i in range(self.native.model_value(0))) # Получает фактический ACK/status, отправленный callback transmit.
|
||||
|
||||
def load(self, payload): # Выполняет стандартный INITLOAD/LOAD без TFLASH.
|
||||
self.assertEqual(len(self.send(self.frame(58, struct.pack("<II", 0x200000, len(payload))))), 4) # Подготовка RAM требует точного короткого ACK.
|
||||
self.assertEqual(len(self.send(self.frame(51, payload))), 4) # Правильный LOAD подтверждается только после полной CRC16.
|
||||
|
||||
def program(self, payload): return self.send(self.frame(55, struct.pack("<III", 0x300000, 0x200000, len(payload)))) # Адреса и порядок полей совпадают с program_request periph_28335.
|
||||
|
||||
def assert_protected(self): self.assertEqual(self.native.model_value(5), 0xA55A) # Постоянный сектор A должен сохраняться даже после отказа на любой стадии записи.
|
||||
|
||||
def test_probe_and_success(self): # Полный успешный обмен должен подтвердить физическую Flash до reset.
|
||||
probe = self.frame(55, bytes(12)); self.assertEqual(probe.hex(), "1037000000000000000000000000f066") # Вектор независимо подтверждён setprotocol.dll через protocan.periph28335.with_crc и обнаруживает изменение wire-формата.
|
||||
response = self.send(probe); self.assertEqual(response[:4], bytes((16, 55, 0, 0))); self.assertEqual(self.native.model_value(1), 0) # Probe ничего не стирает и не останавливает действующую программу.
|
||||
payload = image(); self.load(payload); response = self.program(payload); self.assertEqual(response[:4], bytes((16, 55, 0, 0))) # Успешный TFLASH сообщает нулевой статус.
|
||||
self.assertEqual(self.native.model_value(4), 1); self.assertEqual(self.native.model_value(3), 1); self.assert_protected() # Реальный валидатор принимает физическую Flash, и только затем запрошен reset.
|
||||
|
||||
def test_wrong_address_crc_and_timeout(self): # Чужие либо оборванные запросы не должны разрешать запись.
|
||||
payload = image(); self.assertEqual(self.send(self.frame(58, struct.pack("<II", 0x200000, len(payload)), address=17)), b"") # Чужой адрес не получает подтверждения INITLOAD.
|
||||
self.load(payload); self.send(self.frame(58, struct.pack("<II", 0x200000, len(payload)))[:5]); self.native.model_abort() # Таймаут отбрасывает недопринятый запрос и старое подтверждение LOAD.
|
||||
self.assertEqual(self.program(payload)[2:4], b"\x03\x10"); self.assertEqual(self.native.model_value(1), 0) # Без нового LOAD статус 1003 возникает до erase.
|
||||
self.load(payload); damaged = bytearray(self.frame(55, struct.pack("<III", 0x300000, 0x200000, len(payload)))); damaged[-1] ^= 1; self.assertEqual(self.send(damaged), b"") # Неверная CRC TFLASH не вызывает никакого ответа об успехе.
|
||||
self.assertEqual(self.native.model_value(1), 0); self.assert_protected() # Ни timeout, ни повреждение запроса не затронули Flash.
|
||||
|
||||
def test_changed_ram_and_read_failure(self): # CRC32 staging повторно проверяется после успешного LOAD.
|
||||
payload = image(); self.load(payload); self.native.model_corrupt(60); self.assertEqual(self.program(payload)[2:4], b"\x02\x10") # Изменение RAM не скрывается первоначальным успешным ACK LOAD.
|
||||
self.assertEqual(self.native.model_value(1), 0); self.load(payload); self.native.model_fault(0, 0, 1); self.assertEqual(self.program(payload)[2:4], b"\x02\x10") # Ошибка чтения staging также запрещает первый erase.
|
||||
self.assertEqual(self.native.model_value(1), 0); self.assert_protected() # Память приложения и boot остались прежними.
|
||||
|
||||
def test_forbidden_addresses_and_bad_image(self): # Проверяет все опасные границы Flash и неполную таблицу.
|
||||
for address in (0x2FFFFF, 0x338000, 0x33FFF6, 0x33FFF8, 0x380400): # Включает RAM/границу приложения, boot, CSM и OTP.
|
||||
with self.subTest(address=hex(address)): # Ошибка конкретного диапазона видна отдельно в отчёте.
|
||||
self.native.model_reset(); payload = image()[:-2] + section(address, [1, 2]) + bytes(2); self.load(payload); self.assertEqual(self.program(payload)[2:4], b"\x02\x10") # Правильная CRC16 не разрешает опасный LOAD-адрес внутри SCI8.
|
||||
self.assertEqual(self.native.model_value(1), 0); self.assert_protected() # Неверная структура отвергается раньше физического стирания.
|
||||
for payload in (image()[:-2], image() + bytes(2), image()[:-2] + section(contract.APP_DATA, [1]) + bytes(2)): # Проверяет отсутствующий терминатор, хвост и перекрытие секций.
|
||||
self.native.model_reset(); self.load(payload); self.assertEqual(self.program(payload)[2:4], b"\x02\x10"); self.assertEqual(self.native.model_value(1), 0) # Любое нарушение структуры запрещает erase.
|
||||
|
||||
def test_failure_at_each_write_and_retry(self): # Имитирует обрыв каждого этапа программирования и последующее восстановление.
|
||||
for operation in (100, 1, 2, 3): # 100 обозначает erase; три program-порции — две секции данных и последний FAB1.
|
||||
with self.subTest(operation=operation): # Все стадии проверяются независимо на свежей модели Flash.
|
||||
self.native.model_reset(); payload = image(); self.load(payload); self.native.model_fault(operation, 0, 0); self.assertEqual(self.program(payload)[2:4], b"\x16\x00") # Отказ writer передаёт статус ошибки TI вместо успешного ACK.
|
||||
self.assertEqual(self.native.model_value(4), 0); self.assertEqual(self.native.model_value(3), 0); self.assert_protected() # Частичный образ не запускается, reset не запрошен, постоянный boot сохранён.
|
||||
self.native.model_fault(0, 0, 0); self.load(payload); self.assertEqual(self.program(payload)[2:4], bytes(2)); self.assertEqual(self.native.model_value(4), 1) # Новый полный LOAD восстанавливает приложение через тот же оставшийся boot.
|
||||
|
||||
def test_failed_ack_prevents_reset(self): # Потеря передачи результата не должна перезапускать контроллер неожиданно для клиента.
|
||||
payload = image(); self.load(payload); self.native.model_fault(0, 1, 0); self.program(payload) # Запись проходит успешно, но transmit возвращает ошибку.
|
||||
self.assertEqual(self.native.model_value(4), 1); self.assertEqual(self.native.model_value(3), 0); self.assert_protected() # Физическая Flash готова, однако boot остаётся в recovery до явного последующего reset.
|
||||
|
||||
if __name__ == "__main__": unittest.main(verbosity=2) # Самостоятельный запуск выводит результаты модели без утверждений о проверке платы.
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Собирает постоянный RS boot и выпускает проверенное Flash-приложение CCS.""" # Все аппаратные пути и исходники передаются проектом платы.
|
||||
import argparse # Разбирает режим полной сборки либо postbuild уже собранного приложения.
|
||||
from datetime import datetime # Восстанавливает двенадцать слов встроенного паспорта текущего prebuild.
|
||||
import importlib.util # Подключает существующие инструменты firmware-info и FAB1 без копирования алгоритмов.
|
||||
import json # Читает проектные настройки и сохраняет отдельные manifest приложения и boot.
|
||||
from pathlib import Path # Разрешает пути независимо от текущего каталога CCS.
|
||||
import re # Разбирает стандартные относительные ссылки PARENT-N-PROJECT_LOC в Eclipse.
|
||||
import shutil # Переносит только проверенный комплект из временного каталога.
|
||||
import struct # Сопоставляет двенадцать 16-битных слов паспорта точным октетам COFF.
|
||||
import subprocess # Запускает TI compiler/OFD/hex2000 без shell и без доступа к программатору.
|
||||
import tempfile # Изолирует объекты и неполные выпуски текущей попытки.
|
||||
import xml.etree.ElementTree as ET # Читает реальные linked resources и include paths выбранной конфигурации CCS.
|
||||
TEMPLATES = Path(__file__).resolve().parents[3] # Каталог tools расположен в templates/c/f28335-rs-boot/tools.
|
||||
LIBRARY = Path(__file__).resolve().parents[1] # Общие исходники и linker boot берутся из того же checkout templates.
|
||||
FLASH = TEMPLATES / "c/protocan-boot/ports/tms320f28335" # Использует единые валидатор FAB1 и writer SCI8.
|
||||
|
||||
def module(name, path): # Загружает существующий инструмент по конкретному пути выбранной библиотеки.
|
||||
spec = importlib.util.spec_from_file_location(name, path) # Избегает случайного импорта другого установленного protocan.
|
||||
result = importlib.util.module_from_spec(spec); spec.loader.exec_module(result); return result # Ошибка общей зависимости останавливает выпуск.
|
||||
|
||||
identity = module("rs_boot_identity", TEMPLATES / "c/firmware-info/tools/identity.py") # Сохраняет общие manifest, SHA-256 и имена архивов.
|
||||
contract = module("rs_boot_contract", TEMPLATES / "python/protocan/f28335_flash.py") # Сохраняет единый формат подписания и проверки FAB1.
|
||||
|
||||
def project_paths(args): # Получает фактические C/ASM и include paths из настроек проекта.
|
||||
project = args.project.resolve(); sources = [] # Проектный каталог содержит .project/.cproject и generated отдельной конфигурации.
|
||||
for link in ET.parse(project / ".project").findall(".//linkedResources/link"): # Заголовки, OBJ и старые linker-файлы не компилируются как исходники.
|
||||
uri = link.findtext("locationURI", ""); prefix, separator, relative = uri.partition("/") # Eclipse хранит корень ссылки отдельно от относительного пути.
|
||||
match = re.fullmatch(r"PARENT-(\d+)-PROJECT_LOC", prefix) # Поддерживает переносимые ссылки любого уровня, не зашивая репозиторий платы.
|
||||
root = project.parents[int(match[1]) - 1] if match else project # PARENT-1 указывает непосредственно на родителя CCS-проекта.
|
||||
if separator and (match or prefix == "PROJECT_LOC") and Path(relative).suffix.lower() in (".c", ".asm"): sources.append((root / relative).resolve()) # Сохраняет только реальные C/ASM linked resources.
|
||||
sources.extend((project / "setgui_firmware_identity").glob("*.c")) # Подключённый ранее паспорт SETGUI остаётся частью приложения.
|
||||
config = next(item for item in ET.parse(project / ".cproject").findall(".//configuration") if item.get("name") == args.configuration) # Выбирает именно Debug либо Release, запрошенный CCS.
|
||||
option = next(item for item in config.findall(".//tool/option") if "compilerID.INCLUDE_PATH" in item.get("superClass", "")) # Сохраняет порядок поиска действующих заголовков платы.
|
||||
includes = []; macros = {"${PROJECT_ROOT}": project, "${PROJECT_LOC}": project, "${CG_TOOL_ROOT}": args.compiler.resolve(), "${BuildDirectory}": args.build_dir.resolve()} # Разрешает только ожидаемые файловые макросы CCS.
|
||||
for item in option.findall("listOptionValue"): # Каждая include-настройка применяется без shell expansion.
|
||||
value = item.get("value", "") # Пустой путь не должен означать случайный текущий каталог.
|
||||
for macro, replacement in macros.items(): value = value.replace(macro, str(replacement)) # Подставляет фактические абсолютные пути текущей конфигурации.
|
||||
if not value or "${" in value: raise ValueError("Не разрешён include path CCS: " + value) # Отсутствующую зависимость требуется исправить до компиляции.
|
||||
includes.append(Path(value)) # Сохраняет приоритет dev_sup перед дополнительными TI-заголовками.
|
||||
return list(dict.fromkeys(sources)), includes # Не компилирует повторную ссылку на один исходник дважды.
|
||||
|
||||
def compile_image(args, sources, includes, linker, output, optimize=False, api=False): # Создаёт самостоятельный COFF с собственным runtime.
|
||||
compiler = args.compiler.resolve(); support = args.support.resolve(); objects = [] # Объекты boot не смешиваются с объектами приложения.
|
||||
with tempfile.TemporaryDirectory(prefix="objects-", dir=output.parent) as directory: # Не использует старые OBJ из Debug либо другой версии исходников.
|
||||
for index, source in enumerate(sources): # Уникальный индекс исключает коллизии одинаковых имён модулей из разных каталогов.
|
||||
obj = Path(directory) / (str(index) + "_" + source.stem + ".obj") # Каждый результат принадлежит только этой попытке сборки.
|
||||
command = [str(compiler / "bin/cl2000.exe"), "-v28", "-ml", "-mt", "--float_support=fpu32", "--abi=coffabi", "--gen_func_subsections=on", "--define=F28335_RS_CPU_HZ=150000000UL"] # Архитектура и частота соответствуют F28335 платы ИБП; частота используется формулой TI Flash API.
|
||||
if source.suffix.lower() == ".c": command.append("--c99") # C99 требуется общим bool/stdint, но не передаётся как режим ассемблера.
|
||||
if optimize: command.append("-O2") # Оптимизация boot уменьшает постоянный сектор A; алгоритмы управления приложения сохраняют штатный режим без оптимизации.
|
||||
command.extend("--include_path=" + str(path) for path in includes) # Пути передаются отдельными аргументами, включая пробелы Windows.
|
||||
subprocess.run(command + ["--compile_only", "--output_file=" + str(obj), str(source)], check=True); objects.append(obj) # Ненулевой код TI останавливает сборку до линковки.
|
||||
command = [str(compiler / "bin/cl2000.exe"), "-v28", "--float_support=fpu32", "--abi=coffabi", "-z", "--rom_model", "--heap_size=0", "--stack_size=0x400", "--warn_sections", "--search_path=" + str(compiler / "lib"), "--search_path=" + str(support / "source")] # Отдельный runtime сохраняет boot работоспособным при пустом приложении.
|
||||
if api: command.append("--search_path=" + str(args.flash_api.resolve() / "lib")) # Библиотека TI берётся из локальной установки и не копируется в templates.
|
||||
command.extend(str(path) for path in objects) # Линкер получает только свежие объекты этой попытки.
|
||||
command.extend([str(linker), str(support / "source/DSP2833x_Headers_nonBIOS.cmd"), "-lrts2800_fpu32.lib", "--map_file=" + str(output.with_suffix(".map")), "--output_file=" + str(output)]) # Общие регистры и runtime согласованы с COFF/FPU32 действующего проекта.
|
||||
if not api: command.append(str(support / "source/rts2800_fpu32_fast_supplement.lib")) # Приложение сохраняет ускоренные sincos/sqrt/div/atan2 в прежней ramfuncs.
|
||||
subprocess.run(command, check=True) # Отсутствие Flash API или переполнение RAM/Flash запрещает выпуск.
|
||||
|
||||
def build_boot(args, output): # Компилирует постоянный сектор A с независимыми исходниками платы.
|
||||
support = args.support.resolve(); port = LIBRARY / "ports/tms320f28335" # Общий аппаратный порт не содержит GPIO конкретного изделия.
|
||||
sources = [args.board.resolve(), LIBRARY / "src/f28335_rs_boot.c", port / "flash_api.c", FLASH / "f28335_flash_app.c", FLASH / "f28335_flash_update.c", TEMPLATES / "c/protocan-boot/src/pcan_boot.c", support / "source/DSP2833x_GlobalVariableDefs.c", support / "source/init28335_func.c", support / "source/DSP2833x_CodeStartBranch.asm"] # UART/GPIO принадлежат только board.c; формат и writer повторно используются из templates.
|
||||
includes = [support / "include", LIBRARY / "include", args.flash_api.resolve() / "include", args.compiler.resolve() / "include"] # Все зависимости TI должны существовать до компиляции.
|
||||
compile_image(args, sources, includes, port / "boot.cmd", output, optimize=True, api=True) # Boot использует только FLASHA и выделенные RAMAPI/RAMFUNCS.
|
||||
document = ET.fromstring(subprocess.check_output([str(args.compiler / "bin/ofd2000.exe"), "-x", str(output)])) # Проверяет фактические LOAD-секции, а не только текст linker-команды.
|
||||
for section in document.findall(".//ti_coff/section"): # Учитывает также Flash LOAD копируемого в RAM кода API.
|
||||
if section.findtext("raw_data") is None or section.findtext("copy") == "true": continue # Незагружаемые секции отладки не программируются.
|
||||
start = int(section.findtext("virtual_addr", "0"), 0); size = int(section.findtext("raw_data_size", "0"), 0) // 2 # OFD хранит LOAD как virtual_addr, размер raw_data измеряет октетами.
|
||||
if not (0x338000 <= start < start + size <= 0x33FF80 or start == 0x33FFF6 and size == 2): raise ValueError("Boot OUT содержит LOAD вне постоянного сектора A") # Запрещает приложение, RAM LOAD, CSM и OTP в установочном образе boot.
|
||||
|
||||
def publish(args): # Проверяет оба независимых OUT перед переносом нового выпуска в постоянный каталог.
|
||||
project = args.project.resolve(); build = args.build_dir.resolve(); metadata_path = build / "generated/firmware_identity.json" # Каждый Debug/Release использует собственный паспорт prebuild.
|
||||
metadata = json.loads(metadata_path.read_text(encoding="utf-8")); settings = json.loads((project / "firmware_identity.json").read_text(encoding="utf-8-sig")) # Не резервирует дополнительную версию после компиляции.
|
||||
if any(metadata.get(field) != settings.get(field) for field in ("product", "target", "version", "flashing")): raise ValueError("Настройки изменились: повторите prebuild и сборку") # Не связывает старый OUT с новым продуктом, номером либо маршрутом прошивки.
|
||||
output = build / "firmware"; output.mkdir(parents=True, exist_ok=True) # Все временные и постоянные результаты принадлежат выбранной конфигурации CCS.
|
||||
with tempfile.TemporaryDirectory(prefix=".rs-release-", dir=build) as directory: # Частичная сборка никогда не заменяет предыдущий выпуск.
|
||||
stage = Path(directory); app = stage / "flash" / (args.artifact + ".out"); app.parent.mkdir() # Flash OUT приложения хранится отдельно от boot.
|
||||
shutil.copy2(build / (args.artifact + ".out"), app); contract.seal_coff(app, args.compiler / "bin/ofd2000.exe") # Заполняет только заранее выделенный FAB1 после проверки всех LOAD-адресов приложения.
|
||||
document = ET.fromstring(subprocess.check_output([str(args.compiler / "bin/ofd2000.exe"), "-x", str(app)])); entry, loaded, sections = contract.coff_sections(document) # Содержимое извлекается официальным TI OFD из реального COFF.
|
||||
built = datetime.fromisoformat(metadata["built_at_utc"]); build_id = metadata["build_id"].encode("ascii"); expected = (1, *metadata["version"], built.year, built.month << 8 | built.day, built.hour << 8 | built.minute, built.second, *(build_id[i] << 8 | build_id[i + 1] for i in range(0, 8, 2))) # Двенадцать слов повторяют публичный firmware_info_static контракт паспорта без изменения версии.
|
||||
if sum(tuple(loaded.get(address + i) for i in range(12)) == expected for address in loaded) != 1: raise ValueError("OUT не содержит единственный паспорт текущего prebuild") # Старый объект, чужая версия либо дублированный паспорт не выпускаются.
|
||||
serial = stage / "rs" / (args.artifact + "_flash_rs.bin"); serial.parent.mkdir() # RS-клиент выбирает только SCI8 приложения, исключая постоянный boot.
|
||||
subprocess.run([str(args.compiler / "bin/hex2000.exe"), str(app), "-boot", "-sci8", "-b", "-o", str(serial), "-map", str(serial.with_suffix(".bin.map"))], check=True) # Официальный конвертор сохраняет адресные секции TI SCI8.
|
||||
contract.parse_sci8(serial.read_bytes()); boot = stage / "boot" / (args.artifact + "_boot.out"); boot.parent.mkdir(); build_boot(args, boot) # Проверяет BIN независимо от OUT и создаёт отдельный проверенный установочный boot.
|
||||
shutil.copy2(build / (args.artifact + ".map"), app.with_suffix(".map")) # Карта соответствует неизменным адресам приложения до заполнения заголовка.
|
||||
for image, kind in ((app, "COFF-FLASH"), (serial, "SCI8-FLASH"), (boot, "COFF-RS-BOOT")): # Каждый тип получает собственный manifest и SHA-256 точных байтов.
|
||||
specific = dict(metadata, execution="BOOTLOADER" if image == boot else "FLASH", image_format=kind, boot_contract="f28335-universal-v1", boot_protected=True) # Явно разделяет установку постоянного boot и обновление приложения.
|
||||
if image == boot: specific["flashing"] = {"default_loader": "f28335-jtag", "profiles": {"f28335-jtag": {}}} # Постоянный загрузчик устанавливается через JTAG и никогда не выбирается для RS TFLASH.
|
||||
current = stage / "manifest-source.json"; current.write_text(json.dumps(specific, ensure_ascii=False, indent=2), encoding="utf-8"); identity.seal(image, current); identity.verify(image, image.with_suffix(image.suffix + ".manifest.json")) # Все стороны используют существующие проверенные средства firmware-info.
|
||||
(stage / "manifest-source.json").unlink(); shutil.copy2(metadata_path, stage / "firmware_identity.json") # В архив попадает один исходный паспорт текущей компиляции.
|
||||
destination = output / identity.build_folder_name(metadata); shutil.copytree(stage, destination, dirs_exist_ok=True) # Сохраняет предыдущие архивы и переносит только полностью проверенный выпуск.
|
||||
shutil.copy2(app, build / app.name) # Основной OUT CCS получает тот же FAB1, что опубликованные RS BIN и Flash OUT.
|
||||
if settings.get("copy_to") and not args.no_export: identity.export_build(destination, (project / settings["copy_to"]).resolve(), "rs/" + serial.name) # Установленный проектный export проверяет BIN в папке rs и применяется после всех локальных проверок.
|
||||
print("[firmware-build] RS boot package: " + str(destination)); return destination # Показывает точный постоянный каталог для JTAG и последующего RS-обновления.
|
||||
|
||||
def main(): # Одна точка входа используется проектной обёрткой и проверкой воспроизводимости.
|
||||
parser = argparse.ArgumentParser(description=__doc__); parser.add_argument("command", choices=("build", "postbuild")) # build компилирует приложение; postbuild использует OUT, уже собранный CCS.
|
||||
for name in ("project", "compiler", "build-dir", "support", "board", "app-linker", "flash-api"): parser.add_argument("--" + name, type=Path, required=True) # Все аппаратные исходники и установки выбирает проект-потребитель.
|
||||
parser.add_argument("--artifact", required=True); parser.add_argument("--configuration", default="Debug", choices=("Debug", "Release")); parser.add_argument("--no-export", action="store_true") # Локальная проверка может отключить дополнительное копирование в GUI.
|
||||
args = parser.parse_args(); args.compiler = args.compiler.resolve(); args.build_dir = args.build_dir.resolve(); args.build_dir.mkdir(parents=True, exist_ok=True) # Нормализует только ожидаемые пути без shell expansion.
|
||||
if Path(args.artifact).name != args.artifact or any(char in args.artifact for char in '\\/:*?"<>|'): parser.error("artifact должен быть именем OUT без расширения") # Имя файла не может выйти за выбранный каталог выпуска.
|
||||
if args.command == "build": # Ручной полный build полезен для проверки проекта вне запущенной IDE.
|
||||
sources, includes = project_paths(args); compile_image(args, sources, includes, args.app_linker.resolve(), args.build_dir / (args.artifact + ".out")) # Использует действующие linked resources без ручного списка копий исходников.
|
||||
publish(args) # Ненулевые коды компиляции, конверсии и проверки распространяются в CCS.
|
||||
|
||||
if __name__ == "__main__": main() # Импорт тестами не запускает компилятор и не изменяет рабочую версию.
|
||||
@@ -0,0 +1,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() # Позволяет выполнить проверку отдельно от полного набора тестов библиотеки.
|
||||
File diff suppressed because one or more lines are too long
@@ -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() # Любое нарушение общего итератора возвращает ненулевой код процесса.
|
||||
@@ -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()) # Возвращает командной оболочке фактический результат подготовки.
|
||||
@@ -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.
|
||||
@@ -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,19 +45,45 @@ 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)
|
||||
# A clean application built with a modified templates checkout is dirty too.
|
||||
known = source["commit"] is not None and dependency["commit"] is not None
|
||||
@@ -65,6 +100,7 @@ def prepare(repository: Path, templates: Path, config: Path, output: Path) -> di
|
||||
"version": version, "build_id": build_id, "built_at_utc": stamp.isoformat(),
|
||||
"source": source, "templates": dependency,
|
||||
"identity_contract": 1, "identity_register_count": 12}
|
||||
if "flashing" in settings: metadata["flashing"] = flashing # Переносит все CAN/RS-профили в prebuild, manifest, архив и комплект Публикатора без изменения бинарного протокола паспорта.
|
||||
constants = dict(zip(("FIRMWARE_VERSION_MAJOR", "FIRMWARE_VERSION_MINOR", "FIRMWARE_VERSION_PATCH"), version))
|
||||
constants.update(dict(zip(("FI_YEAR", "FI_MONTH", "FI_DAY", "FI_HOUR", "FI_MINUTE", "FI_SECOND"),
|
||||
(stamp.year, stamp.month, stamp.day, stamp.hour, stamp.minute, stamp.second))))
|
||||
@@ -102,6 +138,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 +230,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))
|
||||
|
||||
|
||||
|
||||
@@ -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`; приложения подключают ревизию сабмодулем.
|
||||
|
||||
@@ -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 исходники.
|
||||
@@ -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. Полная линковка конкретной прошивки и проверка на плате
|
||||
в этот набор не входят.
|
||||
@@ -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 = >_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 /* Завершает защиту заголовка порта. */
|
||||
@@ -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 /* Завершает защиту тестовой модели аппаратуры. */
|
||||
@@ -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") # Подтверждает именно компиляцию объектов, не аппаратные испытания или полную линковку прошивки.
|
||||
@@ -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. */
|
||||
@@ -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-тест только после всех обязательных проверок. */
|
||||
} /* Завершает независимую проверку переносимого ядра. */
|
||||
@@ -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 *`; библиотека
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user