Merge origin/ump-v4 into main

Combine Flash CAN updates, signal processing and HOPE pin fixes with current Modbus/GAS, STM32 ports and CCS release exports. Resolve protocol, documentation and Qt conflicts; deduplicate Android JNI sources and include gauge/IGBT sources in host builds.
This commit is contained in:
2026-11-07 01:32:27 +03:00
136 changed files with 8828 additions and 821 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "third_party/kdiff3"]
path = third_party/kdiff3
url = https://invent.kde.org/sdk/kdiff3.git
+4
View File
@@ -63,6 +63,10 @@ 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 и архив |
+1 -1
View File
@@ -9,5 +9,5 @@ if(NOT WIN32)
endif()
add_library(candle SHARED candle.c candle_ctrl_req.c candle.def)
target_compile_definitions(candle PRIVATE UNICODE _UNICODE)
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32)
target_link_libraries(candle PRIVATE setupapi winusb ole32 advapi32 cfgmgr32) # Configuration Manager сопоставляет интерфейсы CAN и DFU по родительскому USB-узлу.
target_include_directories(candle PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+149 -4
View File
@@ -34,6 +34,86 @@
#include "candle_defs.h"
#include "candle_ctrl_req.h"
#include "ch_9.h"
#include <cfgmgr32.h> /* Сопоставляет интерфейсы CAN и DFU по общему родительскому USB-устройству Windows. */
static DWORD candle_dfu_parent(const wchar_t *path, DEVINST *parent) /* Возвращает родителя интерфейса, чтобы команда не попала в другой подключённый адаптер. */
{ /* Начало поиска физического USB-устройства по пути выбранного CAN-интерфейса. */
HDEVINFO devices = SetupDiCreateDeviceInfoList(NULL, NULL); /* Создаёт временный набор SetupAPI без перечисления посторонних устройств. */
if (devices == INVALID_HANDLE_VALUE) return GetLastError(); /* Сохраняет причину отказа создания набора Windows. */
SP_DEVICE_INTERFACE_DATA iface = {0}; /* Подготавливает описание интерфейса для открытия по точному пути. */
iface.cbSize = sizeof(iface); /* Передаёт SetupAPI размер структуры текущей разрядности процесса. */
SP_DEVINFO_DATA info = {0}; /* Получает идентификатор узла PnP выбранного интерфейса. */
info.cbSize = sizeof(info); /* Указывает размер структуры устройства согласно контракту SetupAPI. */
DWORD size = 0, error = ERROR_SUCCESS; /* Хранит требуемое число байтов и итоговый код Windows. */
if (!SetupDiOpenDeviceInterfaceW(devices, path, 0, &iface)) error = GetLastError(); /* Открывает только заданный путь, сохраняя ошибку исчезнувшего устройства. */
if (!error) SetupDiGetDeviceInterfaceDetailW(devices, &iface, NULL, 0, &size, NULL); /* Узнаёт объём буфера переменной длины для пути USB-интерфейса. */
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail = size ? malloc(size) : NULL; /* Выделяет буфер только после успешного определения его размера. */
if (!error && !detail) error = size ? ERROR_NOT_ENOUGH_MEMORY : GetLastError(); /* Отличает нехватку памяти от ошибки поиска описания. */
if (detail) { /* Обрабатывает описание только при наличии выделенного буфера. */
detail->cbSize = sizeof(*detail); /* Согласует размер заголовка с архитектурой Win32 или Win64. */
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, size, NULL, &info)) error = GetLastError(); /* Получает узел интерфейса из SetupAPI. */
else if (CM_Get_Parent(parent, info.DevInst, 0) != CR_SUCCESS) error = ERROR_NOT_FOUND; /* Получает общий родительский узел составного USB-адаптера. */
free(detail); /* Освобождает описание после получения идентификатора родителя. */
} /* Завершает работу с описанием интерфейса. */
SetupDiDestroyDeviceInfoList(devices); /* Освобождает набор SetupAPI при любом результате поиска. */
return error; /* Возвращает ноль при успехе либо причину отказа Windows. */
} /* Завершает сопоставление интерфейса с физическим USB-устройством. */
uint32_t __stdcall candle_dev_enter_dfu(const wchar_t *can_path, uint8_t status[6]) /* Возвращает код Windows и шестибайтовый DFU_GETSTATUS согласно DFU 1.1. */
{ /* Начало отдельного сеанса управляющего интерфейса обновления. */
if (!can_path || !status) return ERROR_INVALID_PARAMETER; /* Запрещает отсутствие точного пути адаптера или выходного буфера статуса. */
memset(status, 0, 6); /* Исключает использование неинициализированных полей ответа при ошибке USB. */
DEVINST wanted_parent = 0; /* Запоминает физический адаптер, выбранный пользователем до остановки CAN. */
DWORD error = candle_dfu_parent(can_path, &wanted_parent); /* Ищет родителя по сохранённому пути CAN, не заменяя его первым найденным устройством. */
if (error) return error; /* Прерывает переход, если выбранный адаптер уже исчез из PnP. */
GUID guid = {0xc25b4308, 0x04d3, 0x11e6, {0xb3, 0xea, 0x60, 0x57, 0x18, 0x9e, 0x64, 0x43}}; /* GUID интерфейса обновления из дескриптора CANable 2.5. */
HDEVINFO devices = SetupDiGetClassDevsW(&guid, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); /* Перечисляет только подключённые интерфейсы обновления CANable 2.5. */
if (devices == INVALID_HANDLE_VALUE) return GetLastError(); /* Возвращает отказ SetupAPI без попытки послать команду. */
error = ERROR_NOT_FOUND; /* Отсутствие интерфейса обновления не считается успешным входом в DFU. */
for (DWORD index = 0; ; ++index) { /* Проверяет интерфейсы до точного совпадения физического устройства. */
SP_DEVICE_INTERFACE_DATA iface = {0}; /* Создаёт независимое описание очередного DFU-интерфейса. */
iface.cbSize = sizeof(iface); /* Задаёт обязательный размер структуры SetupAPI. */
if (!SetupDiEnumDeviceInterfaces(devices, NULL, &guid, index, &iface)) { /* Завершает обход либо сообщает ошибку перечисления. */
if (GetLastError() != ERROR_NO_MORE_ITEMS) error = GetLastError(); /* Отличает штатный конец списка от отказа Windows. */
break; /* Выходит из обхода после исчерпания интерфейсов или ошибки. */
} /* Завершает проверку результата перечисления. */
DWORD size = 0; /* Хранит размер описания очередного интерфейса в байтах. */
SetupDiGetDeviceInterfaceDetailW(devices, &iface, NULL, 0, &size, NULL); /* Определяет буфер для полного пути интерфейса обновления. */
if (!size) { error = GetLastError(); break; } /* Не продолжает работу с неполным результатом SetupAPI. */
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail = malloc(size); /* Выделяет описание с достаточным местом для USB-пути. */
if (!detail) { error = ERROR_NOT_ENOUGH_MEMORY; break; } /* Не допускает обращения к отсутствующему буферу. */
detail->cbSize = sizeof(*detail); /* Сохраняет ABI описания интерфейса Windows. */
if (!SetupDiGetDeviceInterfaceDetailW(devices, &iface, detail, size, NULL, NULL)) { /* Получает путь, который можно передать CreateFileW. */
error = GetLastError(); free(detail); break; /* Сохраняет ошибку и освобождает буфер перед выходом. */
} /* Завершает чтение пути интерфейса. */
DEVINST parent = 0; /* Хранит родителя текущего кандидата на обновление. */
if (candle_dfu_parent(detail->DevicePath, &parent) || parent != wanted_parent) { /* Исключает каждый интерфейс другого физического адаптера. */
free(detail); continue; /* Освобождает описание неподходящего кандидата и продолжает поиск. */
} /* Завершает проверку принадлежности выбранному USB-адаптеру. */
HANDLE file = CreateFileW(detail->DevicePath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); /* Эксклюзивно открывает интерфейс обновления выбранного устройства для WinUSB. */
free(detail); /* Путь больше не нужен после открытия системного дескриптора. */
if (file == INVALID_HANDLE_VALUE) { error = GetLastError(); break; } /* Не скрывает отсутствие доступа или занятость интерфейса. */
WINUSB_INTERFACE_HANDLE usb = NULL; /* Хранит отдельный WinUSB-сеанс интерфейса обновления без CAN bulk endpoint. */
error = ERROR_SUCCESS; /* Сбрасывает исходное отсутствие интерфейса после точного совпадения устройства. */
if (!WinUsb_Initialize(file, &usb)) error = GetLastError(); /* Проверяет наличие рабочего WinUSB-драйвера DFU runtime. */
USB_INTERFACE_DESCRIPTOR descriptor = {0}; /* Получает реальный номер и класс открытого интерфейса. */
if (!error && !WinUsb_QueryInterfaceSettings(usb, 0, &descriptor)) error = GetLastError(); /* Не предполагает, что произвольный WinUSB-интерфейс поддерживает DFU. */
if (!error && (descriptor.bInterfaceClass != 0xFE || descriptor.bInterfaceSubClass != 1 || descriptor.bInterfaceProtocol != 1)) error = ERROR_NOT_SUPPORTED; /* Проверяет класс приложения, подкласс DFU и runtime-протокол по DFU 1.1. */
ULONG timeout = 500, transferred = 0; /* Ограничивает управляющий USB-запрос 500 миллисекундами и учитывает длину ответа. */
if (!error && !WinUsb_SetPipePolicy(usb, 0, PIPE_TRANSFER_TIMEOUT, sizeof(timeout), &timeout)) error = GetLastError(); /* Устанавливает тайм-аут endpoint 0, чтобы отсутствующий ответ не блокировал программу. */
WINUSB_SETUP_PACKET packet = {0}; /* Обнуляет поля USB SETUP перед запросом перехода в загрузчик. */
packet.RequestType = 0x21; packet.Request = 0; packet.Value = 1000; packet.Index = descriptor.bInterfaceNumber; /* DFU_DETACH: class/interface OUT, запрос 0, ожидаемое время отключения 1000 мс. */
if (!error && !WinUsb_ControlTransfer(usb, packet, NULL, 0, &transferred, NULL)) error = GetLastError(); /* Отправляет DETACH и требует подтверждённой USB-транзакции. */
packet.RequestType = 0xA1; packet.Request = 3; packet.Value = 0; packet.Length = 6; /* DFU_GETSTATUS: class/interface IN, запрос 3, ответ ровно 6 байт по DFU 1.1. */
if (!error && !WinUsb_ControlTransfer(usb, packet, status, 6, &transferred, NULL)) error = GetLastError(); /* Проверяет результат до отложенного перехода прошивки через 300 мс. */
if (!error && transferred != 6) error = ERROR_INVALID_DATA; /* Не трактует укороченный ответ как подтверждение входа в загрузчик. */
if (usb) WinUsb_Free(usb); /* Освобождает WinUSB до появления загрузчика с другим VID/PID. */
CloseHandle(file); /* Освобождает файловый дескриптор после успеха и при любой ошибке USB. */
break; /* Не отправляет DETACH повторно и не переключается на другой адаптер при отказе. */
} /* Завершает поиск и обработку единственного выбранного адаптера. */
SetupDiDestroyDeviceInfoList(devices); /* Освобождает перечень интерфейсов при любом результате. */
return error; /* Статус прошивки возвращается отдельно от системной ошибки USB. */
} /* Завершает управление DFU runtime без записи flash или изменения образа. */
static bool candle_dev_interal_open(candle_handle hdev);
@@ -656,6 +736,30 @@ bool __stdcall DLL candle_dev_open(candle_handle hdev)
}
bool __stdcall DLL candle_dev_get_product(candle_handle hdev, wchar_t *product, uint32_t capacity) /* Читает USB Product String открытого адаптера в буфер UTF-16; capacity измеряется в wchar_t, включая завершающий ноль. */
{ /* Начало чтения отображаемого имени без изменения режима CAN. */
if (!product || !capacity) return false; /* Не пишет в отсутствующий либо пустой выходной буфер. */
product[0] = 0; /* Сохраняет пустую строку при любом отказе чтения дескриптора. */
candle_device_t *dev = (candle_device_t*)hdev; /* Получает уже открытый WinUSB-сеанс выбранного физического адаптера. */
if (!dev || !dev->winUSBHandle) return false; /* Не открывает устройство заново и не обращается к закрытому интерфейсу. */
USB_DEVICE_DESCRIPTOR descriptor = {0}; /* Получает индекс строки продукта из стандартного дескриптора устройства. */
ULONG received = 0; /* Хранит фактически полученное количество байтов управляющего ответа. */
if (!WinUsb_GetDescriptor(dev->winUSBHandle, 1, 0, 0, (PUCHAR)&descriptor, sizeof(descriptor), &received) || received != sizeof(descriptor)) return false; /* Тип 1 обозначает Device Descriptor по USB 2.0; неполный ответ не используется. */
UCHAR string_index = descriptor.iProduct; /* Для обычных адаптеров использует объявленный устройством индекс USB Product String. */
if (descriptor.idVendor == 0x1D50 && descriptor.idProduct == 0x606F) string_index = 2; /* В CANable Candlelight имя продукта имеет индекс 2; WinUSB может подменять iProduct индексом имени интерфейса, как описано в штатном примере проекта. */
if (!string_index) return false; /* Нулевой iProduct означает, что устройство не предоставляет имя продукта. */
UCHAR raw[256] = {0}; /* Буфер вмещает строковый дескриптор USB, чей однобайтовый bLength ограничен 255 байтами. */
USHORT language = 0x0409; /* Использует английский идентификатор USB как резервный вариант для устройств без списка языков. */
if (WinUsb_GetDescriptor(dev->winUSBHandle, 3, 0, 0, raw, sizeof(raw), &received) && received >= 4 && raw[0] >= 4 && raw[1] == 3) language = (USHORT)(raw[2] | ((USHORT)raw[3] << 8)); /* Тип 3 и индекс 0 возвращают список LANGID; выбирает первый язык, предоставленный адаптером. */
if (!WinUsb_GetDescriptor(dev->winUSBHandle, 3, string_index, language, raw, sizeof(raw), &received)) return false; /* Читает именно строку продукта на поддерживаемом устройством языке. */
if (received < 2 || raw[1] != 3 || raw[0] < 2 || raw[0] > received || (raw[0] & 1)) return false; /* Проверяет тип, размер и чётное число байтов UTF-16 перед копированием в выходной буфер. */
uint32_t count = (raw[0] - 2) / 2; /* Исключает двухбайтовый заголовок и переводит длину UTF-16LE из байтов в число кодовых единиц. */
if (count >= capacity) return false; /* Не обрезает индивидуальный номер в конце имени и оставляет место для завершающего нуля. */
for (uint32_t index = 0; index < count; ++index) product[index] = (wchar_t)(raw[2 + index * 2] | ((USHORT)raw[3 + index * 2] << 8)); /* Явно собирает кодовые единицы UTF-16LE без предположений о выравнивании массива байтов. */
product[count] = 0; /* Завершает строку для безопасного чтения через ctypes и отображения в Qt. */
return count > 0; /* Пустое имя оставляет интерфейсу стандартную резервную подпись адаптера. */
} /* Завершает получение настоящего имени из прошивки USB-адаптера. */
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us)
{
return candle_ctrl_get_timestamp(hdev, timestamp_us);
@@ -823,6 +927,38 @@ bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch)
return candle_ctrl_set_device_mode(dev, ch, CANDLE_DEVMODE_RESET, 0);
}
bool __stdcall DLL candle_channel_start_configured(candle_handle hdev, uint8_t ch, uint32_t flags) /* Новый вход сохраняет прежний ABI start и позволяет отключать HW_TIMESTAMP. */
{ /* Явно выбранные Python-флаги должны попасть в прошивку без автоматического дополнения. */
return candle_ctrl_set_device_mode((candle_device_t*)hdev, ch, CANDLE_DEVMODE_START, flags); /* Передаём точную маску после проверки возможностей на уровне транспорта. */
} /* Завершение запуска с явно заданными параметрами. */
bool __stdcall DLL candle_channel_get_data_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap) /* Экспортируем чтение FD-пределов для расчёта тайминга на любой частоте CAN. */
{ /* Общая форма capability позволяет повторно использовать валидатор номинального тайминга. */
return candle_ctrl_data_capability((candle_device_t*)hdev, ch, cap); /* USB-запрос возвращает реальные пределы выбранного канала. */
} /* Завершение получения возможностей быстрой фазы. */
bool __stdcall DLL candle_channel_set_termination(candle_handle hdev, uint8_t ch, uint32_t state) /* Значения 0 и 1 соответствуют OFF и ON стандарта gs_usb. */
{ /* Проверка исключает отправку неопределённого состояния в прошивку. */
return state <= 1 && candle_ctrl_termination((candle_device_t*)hdev, ch, &state, true); /* Выполняем запись только для допустимого состояния. */
} /* Завершение переключения терминатора. */
bool __stdcall DLL candle_channel_get_termination(candle_handle hdev, uint8_t ch, uint32_t *state) /* Чтение состояния не меняет топологию CAN. */
{ /* Используем тот же формат 32-битного значения, что и при записи. */
return candle_ctrl_termination((candle_device_t*)hdev, ch, state, false); /* false выбирает USB_DIR_IN и GET_TERMINATION. */
} /* Завершение чтения терминатора. */
bool __stdcall DLL candle_channel_get_diagnostics(candle_handle hdev, uint8_t ch, uint32_t *state, uint32_t *rxerr, uint32_t *txerr) /* Сохраняем счётчики, которые старый get_state отбрасывал. */
{ /* Диагностика выполняется только для уже открытого устройства. */
candle_device_state_t data = {0}; /* Полная структура протокола содержит три слова: состояние, RX и TX. */
if (!candle_ctrl_get_state((candle_device_t*)hdev, ch, &data)) { /* Не возвращаем нулевые счётчики как будто запрос был успешным. */
return false; /* Причина USB-отказа уже записана управляющим запросом. */
} /* Дальнейшее копирование относится только к подтверждённому ответу. */
*state = data.state; /* Сохраняем состояние Error-active, Warning, Passive, Bus-off, Stopped или Sleeping. */
*rxerr = data.rxerr; /* Счётчик ошибок приёма измеряется в единицах CAN-контроллера. */
*txerr = data.txerr; /* Счётчик ошибок передачи помогает отличать отсутствие ACK от ошибок приёма. */
return true; /* Все три выходных значения относятся к одному USB-ответу. */
} /* Завершение расширенной диагностики канала. */
/* Write len bytes from buf to the OUT pipe, aborting after 300 ms.
* Writes are serialised by writeMutex in CandleApiInterface so dev->txEvent
* is never accessed by two threads simultaneously. */
@@ -1017,14 +1153,13 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
}
dev->rxurbs[urb_num].pending = false;
/* Minimum: classic CAN header (12 bytes) + at least 8 data bytes = 20 bytes */
static const DWORD classic_min = sizeof(candle_frame_t) - 4;
if (bytes_transfered < classic_min) {
const DWORD header_size = 12; /* Заголовок gs_usb занимает 12 байт в Classic и FD; короткий FD может не содержать восьми байтов данных. */
if (bytes_transfered < header_size) { /* До чтения flags и DLC проверяем наличие полного заголовка. */
candle_prepare_read(dev, urb_num);
candle_logf(L"fd read too small urb=%u bytes=%lu min=%lu",
urb_num,
bytes_transfered,
classic_min);
header_size); /* Минимальная длина здесь относится к заголовку, а payload проверяется после определения формата. */
dev->last_error = CANDLE_ERR_READ_SIZE;
return false;
}
@@ -1047,6 +1182,11 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
if (is_fd_frame) {
/* can_dlc is at byte offset 8 in both classic and FD wire frames. */
const uint8_t raw_dlc = dev->rxurbs[urb_num].buf[8];
if (raw_dlc > 15) { /* FD DLC является четырёхбитным кодом и не может содержать значения выше 15. */
candle_prepare_read(dev, urb_num); /* Повреждённый USB-пакет не должен останавливать последующий приём. */
dev->last_error = CANDLE_ERR_READ_SIZE; /* Неверный DLC считается ошибкой формата входного кадра. */
return false; /* Не превращаем повреждённый DLC в пустой успешный FD-кадр. */
} /* Далее длина безопасно вычисляется по таблице CAN FD. */
const DWORD data_len = candle_dlc_to_len(raw_dlc);
const DWORD min_size = 12 + data_len; /* header + data, without timestamp */
@@ -1077,6 +1217,11 @@ bool __stdcall DLL candle_fd_frame_read(candle_handle hdev, candle_fd_frame_t *f
}
/* else: timestamp stays zero from memset above */
} else {
if (bytes_transfered < sizeof(candle_frame_t) - sizeof(uint32_t)) { /* Classic-пакет обязан содержать весь восьмибайтовый буфер данных, даже при меньшем DLC. */
candle_prepare_read(dev, urb_num); /* Возобновляем соответствующий URB после отклонения неполного пакета. */
dev->last_error = CANDLE_ERR_READ_SIZE; /* Не допускаем чтение неполученных данных из локальной структуры. */
return false; /* Повреждённый Classic-пакет не попадает в Python через FD API. */
} /* Минимальный размер Classic теперь проверен отдельно от коротких FD. */
/* Classic CAN frame — copy into FD struct, fixing the timestamp position */
candle_frame_t classic;
DWORD copy_len = (bytes_transfered < sizeof(classic)) ? bytes_transfered : sizeof(classic);
+12
View File
@@ -3,6 +3,8 @@
; прикладные регистры SETProtocol.
EXPORTS
candle_dev_get_product ; Имя продукта из USB-дескриптора прошивки для списка адаптеров SETGUI.
candle_dev_enter_dfu ; Переход через отдельный DFU runtime-интерфейс того же физического USB-адаптера.
candle_list_scan
candle_list_free
candle_list_length
@@ -31,3 +33,13 @@ EXPORTS
candle_frame_data
candle_frame_timestamp_us
candle_dev_last_error
candle_channel_start_configured ; Точная маска запуска с управляемыми аппаратными метками.
candle_channel_get_data_capabilities ; Пределы тайминга быстрой фазы CAN FD.
candle_channel_set_termination ; Переключение встроенного терминатора.
candle_channel_get_termination ; Чтение состояния встроенного терминатора.
candle_channel_get_diagnostics ; Состояние и счётчики ошибок CAN.
candle_channel_set_data_timing ; Тайминг фазы данных CAN FD.
candle_fd_frame_send ; Отправка полного кадра CAN FD до 64 байт.
candle_fd_frame_read ; Приём FD и Classic без обрезания данных.
candle_fd_frame_type ; Различение RX, TX echo и ошибок FD.
candle_fd_frame_id ; Получение CAN ID из структуры FD.
+7
View File
@@ -246,7 +246,9 @@ bool __stdcall DLL candle_list_length(candle_list_handle list, uint8_t *len);
bool __stdcall DLL candle_dev_get(candle_list_handle list, uint8_t dev_num, candle_handle *hdev);
bool __stdcall DLL candle_dev_get_state(candle_handle hdev, candle_devstate_t *state);
wchar_t * __stdcall DLL candle_dev_get_path(candle_handle hdev);
bool __stdcall DLL candle_dev_get_product(candle_handle hdev, wchar_t *product, uint32_t capacity); /* Возвращает USB Product String в UTF-16; capacity включает завершающий ноль и измеряется в wchar_t. */
bool __stdcall DLL candle_dev_open(candle_handle hdev);
uint32_t __stdcall DLL candle_dev_enter_dfu(const wchar_t *can_path, uint8_t status[6]); /* Переводит выбранный физический адаптер в DFU и возвращает Win32-код плюс ответ DFU_GETSTATUS. */
bool __stdcall DLL candle_dev_get_timestamp_us(candle_handle hdev, uint32_t *timestamp_us);
bool __stdcall DLL candle_dev_close(candle_handle hdev);
bool __stdcall DLL candle_dev_free(candle_handle hdev);
@@ -259,6 +261,11 @@ bool __stdcall DLL candle_channel_set_timing(candle_handle hdev, uint8_t ch, can
bool __stdcall DLL candle_channel_set_bitrate(candle_handle hdev, uint8_t ch, uint32_t bitrate);
bool __stdcall DLL candle_channel_start(candle_handle hdev, uint8_t ch, uint32_t flags);
bool __stdcall DLL candle_channel_stop(candle_handle hdev, uint8_t ch);
bool __stdcall DLL candle_channel_start_configured(candle_handle hdev, uint8_t ch, uint32_t flags); /* Запускаем с точной маской без принудительного включения аппаратных меток. */
bool __stdcall DLL candle_channel_get_data_capabilities(candle_handle hdev, uint8_t ch, candle_capability_t *cap); /* Возвращаем пределы тайминга быстрой фазы FD. */
bool __stdcall DLL candle_channel_set_termination(candle_handle hdev, uint8_t ch, uint32_t state); /* Состояние терминатора: 0 — выключен, 1 — включён. */
bool __stdcall DLL candle_channel_get_termination(candle_handle hdev, uint8_t ch, uint32_t *state); /* Чтение не меняет подключение встроенного резистора. */
bool __stdcall DLL candle_channel_get_diagnostics(candle_handle hdev, uint8_t ch, uint32_t *state, uint32_t *rxerr, uint32_t *txerr); /* Передаём состояние CAN и оба счётчика ошибок без потери данных ответа. */
bool __stdcall DLL candle_frame_send(candle_handle hdev, uint8_t ch, candle_frame_t *frame);
bool __stdcall DLL candle_frame_read(candle_handle hdev, candle_frame_t *frame, uint32_t timeout_ms);
+23 -2
View File
@@ -39,8 +39,10 @@ enum {
CANDLE_TIMESTAMP_GET = 6,
/* 7: IDENTIFY, 8: GET_USER_ID, 9: SET_USER_ID (not used here) */
CANDLE_BREQ_DATA_BITTIMING = 10,
/* 11: SET_TERMINATION, not used here */
CANDLE_BREQ_GET_STATE = 12,
CANDLE_BREQ_BT_CONST_EXT = 11, /* Расширенные пределы номинальной и быстрой фазы согласно gs_usb. */
CANDLE_BREQ_SET_TERMINATION = 12, /* Запрос gs_usb 12 переключает встроенный резистор окончания шины. */
CANDLE_BREQ_GET_TERMINATION = 13, /* Запрос gs_usb 13 читает текущее состояние терминатора. */
CANDLE_BREQ_GET_STATE = 14, /* Стандартный GET_STATE имеет номер 14; номер 12 принадлежит терминатору. */
CANDLE_BREQ_BUS_OFF_RECOVERY = 32,
};
@@ -237,3 +239,22 @@ bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel)
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE;
return rc;
}
bool candle_ctrl_data_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *cap) /* Структура gs_usb BT_CONST_EXT содержит десять общих и восемь быстрых полей uint32. */
{ /* Используем массив, поскольку протокол содержит только однородные 32-битные значения без отступов. */
uint32_t fields[18] = {0}; /* 18 полей по 4 байта образуют стандартный ответ размером 72 байта. */
bool rc = usb_control_msg(dev->winUSBHandle, CANDLE_BREQ_BT_CONST_EXT, USB_DIR_IN|USB_TYPE_VENDOR|USB_RECIP_INTERFACE, channel, 0, fields, sizeof(fields)); /* Запрашиваем пределы именно выбранного CAN-канала. */
if (rc) { /* Выходная структура должна меняться только после успешного USB-ответа. */
memcpy(cap, fields, 2 * sizeof(uint32_t)); /* Маска функций и частота CAN общие для обеих фаз. */
memcpy(&cap->tseg1_min, &fields[10], 8 * sizeof(uint32_t)); /* Поля DTSEG1…DBRP_INC заменяют номинальные пределы структуры capability. */
} /* При отказе вызывающий код получит false и не использует нулевые пределы. */
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_GET_BITTIMING_CONST; /* Сохраняем диагностический код чтения аппаратных ограничений. */
return rc; /* Успех означает наличие полной структуры пределов быстрой фазы. */
} /* Завершение запроса расширенных возможностей FD. */
bool candle_ctrl_termination(candle_device_t *dev, uint8_t channel, uint32_t *state, bool write) /* Переключение терминатора использует стандартные SET=12 и GET=13. */
{ /* Состояние передаётся одним 32-битным словом согласно протоколу gs_usb. */
bool rc = usb_control_msg(dev->winUSBHandle, write ? CANDLE_BREQ_SET_TERMINATION : CANDLE_BREQ_GET_TERMINATION, (write ? USB_DIR_OUT : USB_DIR_IN)|USB_TYPE_VENDOR|USB_RECIP_INTERFACE, channel, 0, state, sizeof(*state)); /* Номер CAN-канала передаётся в wValue, а wIndex равен нулю по спецификации. */
dev->last_error = rc ? CANDLE_ERR_OK : CANDLE_ERR_SET_DEVICE_MODE; /* Ошибка настройки доступна Python через candle_dev_last_error. */
return rc; /* Не выдаём успешное переключение при отказе USB-контроллера. */
} /* Завершение управления встроенным окончанием шины. */
+2
View File
@@ -51,4 +51,6 @@ bool candle_ctrl_set_data_bittiming(candle_device_t *dev, uint8_t channel, candl
bool candle_ctrl_get_timestamp(candle_device_t *dev, uint32_t *current_timestamp);
bool candle_ctrl_get_state(candle_device_t *dev, uint8_t channel, candle_device_state_t *data);
bool candle_ctrl_bus_off_recover(candle_device_t *dev, uint8_t channel);
bool candle_ctrl_data_capability(candle_device_t *dev, uint8_t channel, candle_capability_t *cap); /* Читаем пределы быстрой фазы CAN FD отдельным запросом расширенной структуры. */
bool candle_ctrl_termination(candle_device_t *dev, uint8_t channel, uint32_t *state, bool write); /* Передаём состояние терминатора: 0 — выключен, 1 — включён. */
+1
View File
@@ -16,6 +16,7 @@ import shutil
import tempfile
from pathlib import Path
import subprocess
import tempfile # Резервирует номер сборки через временный файл на том же диске, как общий итератор ветки 166.
def git_state(repository: Path) -> dict:
+3 -1
View File
@@ -52,7 +52,8 @@ typedef enum {
PCAN_BOOT_CMD_PING = 0x0CU,
PCAN_BOOT_CMD_CAPABILITIES = 0x0DU,
PCAN_BOOT_CMD_DEVICE_NAME = 0x0EU,
PCAN_BOOT_CMD_MEMORY_INFO = 0x0FU
PCAN_BOOT_CMD_MEMORY_INFO = 0x0FU, /* Сохраняет существующий номер постраничного паспорта памяти. */
PCAN_BOOT_CMD_SELECT_MEMORY = 0x10U /* Выбирает 0=штатное хранилище, 1=внутренняя Flash до передачи метаданных. */
} pcan_boot_command_t;
typedef enum {
@@ -127,6 +128,7 @@ typedef struct {
/** Optional precise result, replacing legacy boolean verify_image. */
pcan_boot_status_t (*verify_status)(void *user, pcan_boot_byte_t slot,
const pcan_boot_manifest_t *manifest);
pcan_boot_status_t (*select_memory)(void *user, pcan_boot_byte_t memory, pcan_boot_config_t *config); /* Необязательный порт явно переключает назначение образа до ERASE; старые порты возвращают INVALID_COMMAND. */
} pcan_boot_port_t;
typedef struct {
+26 -1
View File
@@ -50,6 +50,7 @@ RS Flash должен стирать только B–H, публиковать
Старая самостоятельная Flash-сборка с codestart `0x33FFF6` несовместима с этим
размещением и не должна передаваться как обновление приложения.
Образ — SCI8 BIN (`AA 08`), чётная длина от 24 до **130560 байт** включительно.
Числа в протоколе little endian. Адреса внутри SCI8: сначала старшее 16-битное
слово, затем младшее; каждое слово little endian. В C28x `char` 16-битный,
@@ -154,7 +155,31 @@ SPI-драйвер загрузчика имеет ограничение ожи
Его RAM — 0xE000..0xFFFF; стек — 0x400..0x7FF. Приложение M95M02 использует
0x8000..0xD7FF для кода, 0xD800..0xDFFF для констант/инициализации,
0xE000..0xFFFF для .ebss после перехода в C runtime. Linker резервирует
NOINIT handoff в обоих проектах. CAN не содержит операций записи внутренней Flash.
NOINIT handoff в обоих проектах. TI Flash API временно использует RAM
`0x8000..0x97FF` до запуска приложения. В обновлённом постоянном загрузчике
CAN также записывает внутреннюю Flash приложения B–H; сектор A защищён.
## Внутренняя Flash по CAN
После ENTER_BOOT клиент явно отправляет SELECT_MEMORY (`0x10`, DLC=1,
DATA[0]=1) до BEGIN_IMAGE/BEGIN_COMPAT. Новая сессия по умолчанию использует
EEPROM A/B, поэтому старые клиенты сохраняют прежнее поведение. Выбор памяти
после метаданных запрещён; старый boot отвечает INVALID_COMMAND до ERASE.
CAPABILITIES bit 12 обозначает поддержку записи внутренней Flash.
Flash SCI8 с FAB1 передаётся в неактивный EEPROM-слот как staging. VERIFY
проверяет CRC32 EEPROM, таблицу, непересекающиеся адреса только B–H и FAB1/CRC
будущей Flash. COMMIT повторяет проверку staging, вызывает официальный TI API
со стиранием `0x00FE`, пишет и проверяет данные порциями до 128 слов, публикует
FAB1 последним и проверяет CRC физической Flash. ACK COMMIT приходит после
записи; клиент даёт до 180 секунд и затем отправляет отдельный REBOOT.
EEPROM active и параметры сохраняются; прежний неактивный образ заменяется staging.
В SETGUI: «Прошивка → CAN / ProtoCAN Boot → ProtoCAN: память прошивки →
Внутренняя Flash F28335», файл `*_flash_rs.bin` существующего выпуска.
Сначала требуется обновить постоянный `F28335_CAN_AB.out` через JTAG с маской
только сектора A. В Debug/Release подключён установленный TI Flash API v2.10;
его код и таблицы копируются в RAM перед занятостью общего банка Flash A–H.
## Первичная установка
@@ -8,6 +8,21 @@
* Сброс сессии не удаляет active/pending из EEPROM. */
static pcan_boot_t boot;
static f28335_boot_store_t store;
static unsigned int healthy; /* Результат открытия EEPROM-журнала известен также функции выбора назначения Flash. */
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Flash API доступен только постоянному загрузчику с явной поддержкой обновления B–H. */
#include "f28335_flash_update.h" /* Подключает проверку Flash SCI8 и аппаратный writer с сохранением сектора A. */
#include "f28335_flash_update.c" /* Общий алгоритм компилируется с тем же pcan_boot и разметкой, что этот порт. */
static f28335_flash_update_t flash_update; /* Индекс и кеш занимают ограниченную внутреннюю RAM без загрузки приложения поверх boot. */
static unsigned int flash_selected, flash_staging; /* Явная цель Flash использует только неактивный EEPROM-слот как временное хранилище. */
static pcan_boot_status_t select_memory(void *u, pcan_boot_byte_t memory, pcan_boot_config_t *cfg) /* Выбирает назначение до BEGIN_IMAGE, сохраняя прежний EEPROM-путь по умолчанию. */
{ /* Изменения конфигурации разрешены только при исправном EEPROM-журнале. */
(void)u; if (!healthy) return PCAN_BOOT_STATUS_FLASH_ERROR; /* Ошибка чтения журнала запрещает выбор staging поверх неизвестного active. */
if (memory > 1U) return PCAN_BOOT_STATUS_INVALID_COMMAND; /* Единственные определённые цели: 0=EEPROM A/B и 1=внутренняя Flash. */
flash_selected = memory; flash_update.valid = false; /* Новая цель отменяет прежнее разрешение программирования. */
cfg->slot_count = memory ? 1U : 2U; cfg->active_slot = memory ? 0U : store.active; /* CAN Flash использует один логический слот A, EEPROM сохраняет выбор неактивного A/B. */
return PCAN_BOOT_STATUS_OK; /* Физический staging будет инвалидирован лишь при последующей команде ERASE. */
} /* Никакие данные Flash или EEPROM при выборе цели не изменяются. */
#endif /* Приложение и старые host-модели не подключают Flash API. */
/* Кольцевая очередь: ISR производит кадры, main потребляет. Из 32 элементов
* используется 31, чтобы head==tail однозначно означало пустую очередь.
* Долгие операции SPI запрещены внутри обработчика CAN. */
@@ -16,7 +31,7 @@ static struct { uint32_t id; unsigned int dlc; pcan_boot_byte_t data[8]; } queue
/* reset_requested только планирует reset; Service ждёт завершения TX.
* ready сообщает об окончании старта приложения, confirmed — о сохранённом
* подтверждении слота. healthy отражает результат открытия журнала при Init. */
static unsigned int reset_requested,reset_session,ready,healthy,confirmed;
static unsigned int reset_requested,reset_session,ready,confirmed; /* Состояние перехода и подтверждения приложения отделено от доступности EEPROM. */
static uint32_t sent_at,ready_at;
static unsigned int main_passes;
static unsigned int addressed; /* Разрешает обслуживание только после проверки номера PERIPH и установки согласованных фильтров CAN. */
@@ -49,9 +64,19 @@ static bool transmit(void *u, uint32_t id, const pcan_boot_byte_t *d, pcan_boot_
* эти callbacks возвращают отказ, даже если им удастся передать команду записи.
* ERASE для EEPROM означает инвалидирование manifest выбранного слота. */
static bool erase(void *u,pcan_boot_byte_t slot)
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_prepare(&store,slot); }
{ /* ERASE подготавливает staging; внутренняя Flash остаётся неизменной до VERIFY и COMMIT. */
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Новая цель не меняет прежний формат EEPROM A/B. */
if (flash_selected) { flash_update.valid = false; flash_staging = store.active == 0U ? 1U : 0U; return healthy && f28335_boot_prepare(&store, flash_staging); } /* Инвалидирует только неактивный слот, сохраняя подтверждённый резерв и параметры. */
#endif /* В остальных сборках остаётся штатная подготовка EEPROM-слота. */
(void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_prepare(&store,slot); /* Обычный CAN-клиент сохраняет прежний путь обновления RAM-приложения. */
} /* Подготовка не вызывает TI Flash_Erase. */
static bool write_block(void *u,pcan_boot_byte_t slot,uint32_t offset,const pcan_boot_byte_t *d,pcan_boot_byte_t n)
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_write(&store,slot,offset,d,n); }
{ /* CAN ISR только ставит данные в очередь; запись staging выполняет main. */
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Запись Flash-файла использует тот же проверенный EEPROM writer. */
if (flash_selected) return healthy && f28335_boot_write(&store, flash_staging, offset, d, n); /* Логический DATA_A направляется в выбранный неактивный физический слот. */
#endif /* Обычные A/B-данные сохраняют исходную адресацию. */
(void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_write(&store,slot,offset,d,n); /* Не допускает записи из прикладной сборки. */
} /* Внутренняя Flash ещё не затронута. */
/* Проверка включает CRC EEPROM и структуру SCI8. Сейчас отказ этого слоя
* кодируется CRC_ERROR как общий отказ проверки образа; причины внутри
* парсера SCI8 отдельными статусами протокола не различаются. */
@@ -59,10 +84,21 @@ static pcan_boot_status_t verify(void *u,pcan_boot_byte_t slot,const pcan_boot_m
{
(void)u;
if(!F28335_BOOT_LOADER || !healthy) return PCAN_BOOT_STATUS_INVALID_STATE;
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Для внутренней Flash нужен собственный допустимый диапазон SCI8 и обязательный FAB1. */
if (flash_selected) return f28335_flash_update_validate(&flash_update, &store.io, flash_staging ? F28335_AB_SLOT_B : F28335_AB_SLOT_A, m) ? PCAN_BOOT_STATUS_OK : PCAN_BOOT_STATUS_CRC_ERROR; /* Повторно читает EEPROM и проверяет транспортную CRC, таблицу и CRC будущей Flash до erase. */
#endif /* RAM SCI8 проходит прежнюю проверку приложения EEPROM. */
return f28335_boot_verify(&store,slot,m,0)?PCAN_BOOT_STATUS_OK:PCAN_BOOT_STATUS_CRC_ERROR;
}
static bool commit(void *u,pcan_boot_byte_t slot,const pcan_boot_manifest_t *m)
{ (void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_commit(&store,slot,m); }
{ /* COMMIT выполняет фактическую запись только после подтверждённой проверки образа. */
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Постоянный boot остаётся вне обновляемых секторов B–H. */
if (flash_selected) { /* Синхронный TI API может работать дольше обычного интервала CAN, не создавая pending EEPROM. */
bool result = healthy && F28335Boot_FlashProgram(&flash_update); /* Итог зависит от erase, каждой порции readback и полной CRC физической Flash. */
boot.now_ms = F28335Boot_Now(); boot.last_activity_ms = boot.now_ms; return result; /* Обновляет срок сессии после аппаратной операции, чтобы штатный REBOOT не был отклонён из-за времени записи. */
} /* Ошибка записи оставляет постоянный boot доступным для нового сеанса. */
#endif /* Обычное обновление EEPROM сохраняет trial/CONFIRM. */
(void)u; return F28335_BOOT_LOADER && healthy && f28335_boot_commit(&store,slot,m); /* Публикует только RAM-образ в исходном журнале A/B. */
} /* Итоговый статус и отдельный REBOOT отправляет существующее ядро ProtoCAN. */
/* Подтверждение принадлежит приложению после READY. Загрузчик не может
* подтвердить кандидата сам: иначе исчезнет смысл проверки пробного запуска.
* Разрешение кормления watchdog из ISR открываем только после записи журнала. */
@@ -83,7 +119,12 @@ static unsigned int enter(void *u,pcan_boot_byte_t session)
{
(void)u;
if(!healthy) return 2;
if(F28335_BOOT_LOADER) return 0;
if(F28335_BOOT_LOADER) { /* Постоянный загрузчик открывает сессию без дополнительного reset. */
#if F28335_CAN_FLASH_UPDATE /* Каждая новая сессия должна начинаться со штатного EEPROM-назначения. */
if (boot.state == PCAN_BOOT_STATE_IDLE) (void)select_memory(0, 0U, &boot.config); /* Повторный ENTER текущей сессии не меняет уже выбранную Flash. */
#endif /* Старые сборки не используют переключение назначения. */
return 0; /* Ядро отправит адресный ACK готовности. */
} /* Прикладной handoff проверяется ниже. */
/* No destructive fallback on boards still starting through the SPI ROM. */
if((F28335Boot_RunningSlot()>1 && F28335Boot_RunningSlot()!=F28335_BOOT_FLASH_SLOT) || !confirmed) return 2; /* Только handoff общего загрузчика разрешает переход из EEPROM либо защищённой внутренней Flash; старый SPI-ROM старт остаётся запрещён. */
reset_session=session; reset_requested=1; return 1;
@@ -102,7 +143,10 @@ static bool info(void *u,pcan_boot_byte_t command,pcan_boot_byte_t page,pcan_boo
/* bits: ping, discovery, identity, name, memory, A/B, confirm, rollback,
* separate loader, session timeout, readback, SCI8 validation. */
info32(out,(F28335_BOOT_LOADER || F28335Boot_RunningSlot()<2 || F28335Boot_RunningSlot()==F28335_BOOT_FLASH_SLOT)?0xFFFUL:0x1FUL); /* Flash-приложение с достоверным handoff поддерживает вход в постоянный CAN Boot так же, как подтверждённый EEPROM-образ. */
info32(out+4,F28335_AB_SLOT_BYTES); return true;
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* Дополнительный бит сообщает клиенту поддержку явной внутренней Flash. */
out[1] |= 0x10U; /* Бит 12 общего little-endian capability word означает SELECT_MEMORY и запись B–H. */
#endif /* Старые приложения продолжают сообщать прежние возможности. */
info32(out+4,F28335_AB_SLOT_BYTES); return true; /* Максимальная длина одинакова для staging Flash и EEPROM A/B. */
}
if(command==PCAN_BOOT_CMD_DEVICE_NAME) {
if((unsigned int)page*8U>=name_size) return false; /* Страница после конца выбранного имени не является допустимой частью паспорта. */
@@ -146,6 +190,10 @@ void F28335CanBoot_Init(unsigned int periph_id) /* Один номер плат
cfg.active_slot=store.active;
cfg.slot_count=2; cfg.max_image_size=F28335_AB_SLOT_BYTES; cfg.ack_window=16;
cfg.session_timeout_ms=30000UL;
#if F28335_BOOT_LOADER && F28335_CAN_FLASH_UPDATE /* После reset никакое случайное содержимое RAM не выбирает внутреннюю Flash. */
flash_selected = 0U; flash_staging = F28335_AB_NONE; flash_update.valid = false; /* Возвращает исходную цель и отменяет разрешение записи предыдущей сессии. */
port.select_memory = select_memory; /* Новая команда доступна только сборке с реально подключённым TI API. */
#endif /* Приложение отвечает только через постоянный загрузчик после handoff. */
port.send=transmit; port.erase_slot=erase; port.write_slot=write_block;
port.verify_status=verify; port.set_pending_slot=commit; port.reboot=reboot;
port.confirm_running_slot=confirm; port.enter_boot=enter;
@@ -0,0 +1,86 @@
#include "f28335_flash_update.h" /* Разделяет проверяемый алгоритм и аппаратные вызовы TI Flash API. */
#include <string.h> /* Явно очищает состояние источника перед разбором нового SCI8. */
static unsigned int octet(f28335_flash_update_t *s, uint32_t offset) /* Читает октет через страничный кеш, исключая SPI-транзакцию на каждый байт CRC. */
{ /* Все смещения ограничены фактической длиной принятого файла. */
if (offset >= s->length) { s->fault = true; return 0U; } /* Обрезанное поле не превращается в данные за концом staging. */
if (!s->cache_size || offset < s->cache_at || offset - s->cache_at >= s->cache_size) { /* Загружает новую страницу только при выходе за текущую. */
s->cache_at = offset & ~255UL; /* Выравнивает смещение на страницу из 256 физических октетов. */
s->cache_size = s->length - s->cache_at > 256UL ? 256U : (unsigned int)(s->length - s->cache_at); /* Последнее чтение не включает паддинг CAN за концом образа. */
if (!s->io.read(s->io.user, s->base + s->cache_at, s->cache, s->cache_size)) { s->fault = true; s->cache_size = 0U; return 0U; } /* Любой неполный аппаратный обмен запрещает erase. */
} /* После заполнения кеша извлечение октета не обращается к SPI. */
return s->cache[offset - s->cache_at] & 255U; /* Маска сохраняет восьмибитный wire-формат на C28x. */
} /* Завершает ограниченное чтение октета. */
static uint16_t word(f28335_flash_update_t *s, uint32_t offset) /* SCI8 передаёт младший октет каждого 16-битного слова первым. */
{ /* Объединяет два октета только через проверяемый источник. */
unsigned int low = octet(s, offset); /* Явный порядок вызовов не зависит от порядка вычисления операндов компилятором. */
return (uint16_t)(low | (octet(s, offset + 1UL) << 8)); /* Восстанавливает одно слово CPU из двух октетов SCI8. */
} /* Завершает декодирование слова. */
static uint32_t address_at(f28335_flash_update_t *s, uint32_t offset) /* Адрес таблицы TI содержит старшее слово перед младшим. */
{ /* Порядок слов адреса отличается от порядка октетов внутри слова. */
uint32_t high = word(s, offset); /* Читает старшие 16 бит адреса раньше младших. */
return (high << 16) | word(s, offset + 2UL); /* Получает CPU word-address без перевода в байты. */
} /* Завершает восстановление адреса SCI8. */
static uint16_t image_word(void *user, uint32_t address) /* Предоставляет общему валидатору будущий вид Flash, включая стёртые промежутки. */
{ /* Отсортированный индекс позволяет проверять весь CRC-диапазон последовательно. */
f28335_flash_update_t *s = user; f28335_flash_section_t *p; /* Контекст содержит только предварительно проверенные секции. */
if (s->cursor >= s->count || address < s->section[s->cursor].address) s->cursor = 0U; /* Возврат от заголовка к началу диапазона корректно перематывает индекс. */
while (s->cursor < s->count) { /* Пропускает только секции, целиком лежащие перед запрошенным словом. */
p = &s->section[s->cursor]; /* Обращается к существующему элементу проверенного индекса. */
if (address < p->address) return 0xFFFFU; /* Flash_Erase оставляет незагружаемый промежуток заполненным единицами. */
if (address - p->address < p->words) return word(s, p->offset + 2UL * (address - p->address)); /* Переводит смещение слов секции в октеты SCI8. */
++s->cursor; /* Следующий запрос не должен повторно сканировать уже пройденную секцию. */
} /* После последней секции остаётся только стёртая Flash. */
return 0xFFFFU; /* CRC учитывает также завершающий промежуток диапазона FAB1. */
} /* Сам этот callback никогда не читает аппаратную Flash. */
bool f28335_flash_update_validate(f28335_flash_update_t *s, const f28335_boot_io_t *io, uint32_t base, const pcan_boot_manifest_t *m) /* Проверяет транспорт, таблицу, допустимые адреса и FAB1 перед erase. */
{ /* Любая ошибка оставляет объект непригодным для программирования. */
uint32_t offset = 22UL, crc = 0UL, entry, checked_entry, bytes; unsigned int i, j, headers = 0U, have_entry = 0U; /* Заголовок SCI8 занимает 11 слов, то есть 22 октета. */
f28335_flash_section_t p; bool ended = false; /* Временная секция нужна для вставки в отсортированный индекс. */
if (!s || !io || !io->read || !m) return false; /* Не разыменовывает отсутствующие callbacks и manifest. */
memset(s, 0, sizeof(*s)); s->io = *io; s->base = base; s->length = m->image_size; s->crc = m->image_crc32; /* Отбрасывает кеш и разрешение записи предыдущего образа. */
if (s->length < 24UL || (s->length & 1UL) || s->length > F28335_AB_SLOT_BYTES || word(s, 0UL) != 0x08AAU) return false; /* Принимает только полный чётный SCI8 в пределах staging-слота. */
for (offset = 0UL; offset < s->length; offset += bytes) { /* Независимо проверяет фактически записанную EEPROM, а не только принятые CAN-кадры. */
bytes = s->length - offset > 256UL ? 256UL : s->length - offset; /* Делит чтение на ограниченные страницы без выхода за файл. */
if (!s->io.read(s->io.user, base + offset, s->cache, (unsigned int)bytes)) return false; /* Аппаратная ошибка запрещает последующий erase. */
crc = pcan_boot_crc32_update(crc, s->cache, (size_t)bytes); /* Использует общий CRC32 IEEE, совпадающий с клиентом SETGUI. */
} /* Все октеты staging включены в транспортную CRC32. */
s->cache_size = 0U; if (crc != s->crc) return false; /* Сбрасывает кеш после прямого чтения и отклоняет повреждённую EEPROM. */
entry = address_at(s, 18UL); offset = 22UL; /* Точка входа следует после ключа и восьми резервных слов SCI8. */
if (entry < F28335_FLASH_APP_DATA || entry >= F28335_FLASH_APP_STOP) return false; /* Запуск разрешён только из приложения ниже постоянного сектора A. */
while (offset + 2UL <= s->length) { /* Для каждой секции сначала проверяет наличие поля длины. */
p.words = word(s, offset); offset += 2UL; /* Длина секции выражена адресуемыми словами C28x. */
if (!p.words) { ended = offset == s->length; break; } /* Нулевой терминатор обязан завершать файл без лишнего хвоста. */
if (s->count == F28335_AB_MAX_SECTIONS || offset + 4UL > s->length) return false; /* Не допускает переполнение индекса либо обрезанный адрес секции. */
p.address = address_at(s, offset); offset += 4UL; p.offset = offset; bytes = 2UL * p.words; /* Хранит CPU-адрес и байтовое смещение полезных данных отдельно. */
if (bytes > s->length - offset || p.address < F28335_FLASH_APP_START || p.address >= F28335_FLASH_APP_STOP || p.words > F28335_FLASH_APP_STOP - p.address) return false; /* Исключает RAM, сектор A, CSM, OTP и выход за конец staging. */
if (p.address == F28335_FLASH_APP_START) { if (p.words < F28335_FLASH_APP_HEADER_WORDS) return false; ++headers; } /* Полные 16 слов FAB1 должны публиковаться одной заключительной операцией. */
if (entry >= p.address && entry - p.address < p.words) have_entry = 1U; /* Код entry обязан присутствовать именно в принятой таблице. */
for (i = 0U; i < s->count; ++i) if (p.address < s->section[i].address + s->section[i].words && s->section[i].address < p.address + p.words) return false; /* Повторное программирование пересекающихся диапазонов запрещено. */
for (j = s->count; j && s->section[j - 1U].address > p.address; --j) s->section[j] = s->section[j - 1U]; /* Сортирует индекс по адресу независимо от порядка секций в SCI8. */
s->section[j] = p; ++s->count; offset += bytes; /* Запоминает непересекающуюся секцию и переходит к следующему заголовку. */
} /* Структура файла полностью разобрана без записи Flash. */
if (!ended || headers != 1U || !have_entry || s->fault) return false; /* Требует терминатор, единственный FAB1 и фактический исполняемый entry. */
s->cursor = 0U; if (!f28335_flash_app_validate(image_word, s, &checked_entry) || s->fault || checked_entry != entry) return false; /* Общий валидатор проверяет magic, границы, инверсию entry и CRC будущей Flash. */
s->entry = entry; s->valid = true; return true; /* Разрешает программирование только после всех независимых проверок. */
} /* Ошибка проверки не меняет текущую Flash и журнал активной EEPROM. */
bool f28335_flash_update_program(f28335_flash_update_t *s, const f28335_flash_ops_t *ops) /* Выполняет запись B–H только из полностью проверенного staging. */
{ /* Заголовок FAB1 фиксируется после успешной проверки каждой порции данных. */
uint16_t buffer[128]; unsigned int i, j, n; uint32_t at, left, entry; pcan_boot_manifest_t m; f28335_boot_io_t io; /* Одна порция содержит не более 128 слов, то есть 256 физических октетов. */
if (!s || !s->valid || !ops || !ops->erase || !ops->program || !ops->read) return false; /* Неполный аппаратный контракт не должен начать стирание. */
memset(&m, 0, sizeof(m)); m.image_size = s->length; m.image_crc32 = s->crc; io = s->io; at = s->base; /* Сохраняет исходные параметры перед полной повторной проверкой EEPROM. */
if (!f28335_flash_update_validate(s, &io, at, &m)) return false; /* Изменение staging между VERIFY и COMMIT обнаруживается до первого erase. */
s->valid = false; if (!ops->erase(ops->user)) return false; /* Одно разрешение записи расходуется до изменения Flash; повтор после отказа требует нового VERIFY. */
for (i = 0U; i < s->count; ++i) { /* Программирует обычные данные каждой непересекающейся секции. */
at = s->section[i].address; left = s->section[i].words; /* CPU-курсор и остаток выражены словами. */
if (at == F28335_FLASH_APP_START) { at += F28335_FLASH_APP_HEADER_WORDS; left -= F28335_FLASH_APP_HEADER_WORDS; } /* Пропускает FAB1 до подтверждения всех остальных данных. */
while (left) { /* Большие секции разбивает на порции рабочего буфера. */
n = left > 128UL ? 128U : (unsigned int)left; /* Последняя порция может быть короче 128 слов. */
for (j = 0U; j < n; ++j) buffer[j] = word(s, s->section[i].offset + 2UL * (at + j - s->section[i].address)); /* Распаковывает слова непосредственно из staging EEPROM. */
if (s->fault || !ops->program(ops->user, at, buffer, n)) return false; /* Ошибка чтения, записи или readback оставляет FAB1 неопубликованным. */
at += n; left -= n; /* Продвигает курсоры только после подтверждённой порции. */
} /* Завершает запись данных секции. */
} /* Все обычные слова уже записаны и проверены аппаратным портом. */
for (i = 0U; i < F28335_FLASH_APP_HEADER_WORDS; ++i) buffer[i] = image_word(s, F28335_FLASH_APP_START + i); /* Собирает единственное проверенное описание FAB1 для заключительной записи. */
if (s->fault || !ops->program(ops->user, F28335_FLASH_APP_START, buffer, F28335_FLASH_APP_HEADER_WORDS)) return false; /* Публикует описание только после успешной записи остальных секций. */
return f28335_flash_app_validate(ops->read, ops->user, &entry) && entry == s->entry; /* Итоговый успех требует общей CRC уже физической Flash, а не только поблочного сравнения. */
} /* Решение о CAN-подтверждении и перезапуске остаётся существующему протоколу. */
@@ -0,0 +1,20 @@
#ifndef F28335_FLASH_UPDATE_H /* Один контракт объединяет проверку CAN-образа и аппаратную запись внутренней Flash. */
#define F28335_FLASH_UPDATE_H /* Исключает повторное объявление структур при подключении аппаратного порта. */
#include "f28335_boot_store.h" /* Использует существующий побайтовый доступ к staging-слоту EEPROM и manifest ProtoCAN. */
#include "f28335_flash_app.h" /* Получает границы приложения B–H и проверку завершающего описания FAB1. */
typedef struct { uint32_t address, offset; uint16_t words; } f28335_flash_section_t; /* Адрес CPU считается словами, смещение SCI8 — октетами, длина секции — словами. */
typedef struct { /* Хранит проверенный индекс SCI8, не размещая целый образ во внутренней RAM загрузчика. */
f28335_boot_io_t io; uint32_t base, length, crc, entry; /* Источник EEPROM, начало staging, точная длина, транспортная CRC32 и подтверждённый entry. */
f28335_flash_section_t section[F28335_AB_MAX_SECTIONS]; /* Лимит 64 совпадает с ограничением выпуска SCI8 и ограничивает расход RAM. */
unsigned int count, cursor, cache_size; bool fault, valid; /* Курсор ускоряет последовательную CRC; fault запрещает принятие неполного чтения EEPROM. */
uint32_t cache_at; pcan_boot_byte_t cache[256]; /* Одна страница M95M02 содержит 256 физических октетов, даже при 16-битном char C28x. */
} f28335_flash_update_t; /* Объект принадлежит только main постоянного загрузчика, а не CAN ISR. */
typedef struct { /* Аппаратные операции отделены от формата SCI8 для испытаний обрыва и ошибок TI API. */
bool (*erase)(void *user); /* Стирает только секторы B–H, сохраняя сектор A с постоянным загрузчиком. */
bool (*program)(void *user, uint32_t address, uint16_t *data, unsigned int words); /* Записывает и проверяет одну порцию слов по допустимому CPU-адресу. */
f28335_flash_read_word_t read; void *user; /* Чтение физической Flash подтверждает общий FAB1/CRC после последней записи. */
} f28335_flash_ops_t; /* Никаких неявных операций reset или изменения EEPROM-журнала здесь нет. */
bool f28335_flash_update_validate(f28335_flash_update_t *s, const f28335_boot_io_t *io, uint32_t base, const pcan_boot_manifest_t *m); /* Полностью проверяет staging до первого стирания Flash. */
bool f28335_flash_update_program(f28335_flash_update_t *s, const f28335_flash_ops_t *ops); /* Повторяет проверку, пишет данные и публикует FAB1 последним. */
bool F28335Boot_FlashProgram(f28335_flash_update_t *s); /* Аппаратная реализация подключает официальный TI Flash API только в постоянном загрузчике. */
#endif /* Завершает защиту интерфейса CAN-обновления внутренней Flash. */
@@ -80,6 +80,7 @@
<listOptionValue builtIn="false" value="${PROJECT_ROOT}/../../../vendor/ti/f28335/common/include" />
<!-- Передаёт значение параметра: ${CG_TOOL_ROOT}/include. -->
<listOptionValue builtIn="false" value="${CG_TOOL_ROOT}/include" />
<listOptionValue builtIn="false" value="C:/ti/controlSUITE/libs/utilities/flash_api/2833x/28335/v210/include" /> <!-- Подключает официальный TI Flash API v2.10 F28335 для обновления секторов B–H по CAN. -->
</option>
<!-- Задаёт параметр сборки: Pre-define NAME (- -define, -D). -->
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.compilerID.DEFINE.409484878.f28335" name="Pre-define NAME (--define, -D)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.compilerID.DEFINE" valueType="definedSymbols">
@@ -89,6 +90,7 @@
<listOptionValue builtIn="false" value="EEPROM_M95M02=1" />
<!-- Передаёт значение параметра: F28335_BOOT_LOADER=1. -->
<listOptionValue builtIn="false" value="F28335_BOOT_LOADER=1" />
<listOptionValue builtIn="false" value="F28335_CAN_FLASH_UPDATE=1" /> <!-- Разрешает выбор внутренней Flash только при реально подключённом TI API. -->
<!-- Передаёт значение параметра: _DEBUG. -->
<listOptionValue builtIn="false" value="_DEBUG" />
</option>
@@ -119,11 +121,13 @@
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.LIBRARY.1967188356.f28335" name="Include library file or command file as input (--library, -l)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.LIBRARY" valueType="libs">
<!-- Передаёт значение параметра: rts2800_fpu32.lib. -->
<listOptionValue builtIn="false" value="rts2800_fpu32.lib" />
<listOptionValue builtIn="false" value="Flash28335_API_V210.lib" /> <!-- Официальные erase/program/verify копируются в RAM перед занятостью общего банка Flash. -->
</option>
<!-- Задаёт параметр сборки: Add <dir> to library search path (- -search_path, -i). -->
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.SEARCH_PATH.1097407993.f28335" name="Add &lt;dir&gt; to library search path (--search_path, -i)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.SEARCH_PATH" valueType="libPaths">
<!-- Передаёт значение параметра: ${CG_TOOL_ROOT}/lib. -->
<listOptionValue builtIn="false" value="${CG_TOOL_ROOT}/lib" />
<listOptionValue builtIn="false" value="C:/ti/controlSUITE/libs/utilities/flash_api/2833x/28335/v210/lib" /> <!-- Находит установленную библиотеку TI без добавления её двоичного содержимого в проект. -->
<!-- Передаёт значение параметра: ${PROJECT_ROOT}. -->
<listOptionValue builtIn="false" value="${PROJECT_ROOT}" />
</option>
@@ -234,6 +238,7 @@
<listOptionValue builtIn="false" value="${PROJECT_ROOT}/../../../vendor/ti/f28335/common/include" />
<!-- Передаёт значение параметра: ${CG_TOOL_ROOT}/include. -->
<listOptionValue builtIn="false" value="${CG_TOOL_ROOT}/include" />
<listOptionValue builtIn="false" value="C:/ti/controlSUITE/libs/utilities/flash_api/2833x/28335/v210/include" /> <!-- Подключает официальный TI Flash API v2.10 F28335 для обновления секторов B–H по CAN. -->
</option>
<!-- Задаёт параметр сборки: Pre-define NAME (- -define, -D). -->
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.compilerID.DEFINE.409484878.f28335" name="Pre-define NAME (--define, -D)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.compilerID.DEFINE" valueType="definedSymbols">
@@ -243,6 +248,7 @@
<listOptionValue builtIn="false" value="EEPROM_M95M02=1" />
<!-- Передаёт значение параметра: F28335_BOOT_LOADER=1. -->
<listOptionValue builtIn="false" value="F28335_BOOT_LOADER=1" />
<listOptionValue builtIn="false" value="F28335_CAN_FLASH_UPDATE=1" /> <!-- Разрешает выбор внутренней Flash только при реально подключённом TI API. -->
<!-- Передаёт значение параметра: _DEBUG. -->
</option>
<!-- Задаёт параметр сборки: Releaseging model. -->
@@ -271,11 +277,13 @@
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.LIBRARY.1967188356.f28335" name="Include library file or command file as input (--library, -l)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.LIBRARY" valueType="libs">
<!-- Передаёт значение параметра: rts2800_fpu32.lib. -->
<listOptionValue builtIn="false" value="rts2800_fpu32.lib" />
<listOptionValue builtIn="false" value="Flash28335_API_V210.lib" /> <!-- Официальные erase/program/verify копируются в RAM перед занятостью общего банка Flash. -->
</option>
<!-- Задаёт параметр сборки: Add <dir> to library search path (- -search_path, -i). -->
<option IS_BUILTIN_EMPTY="false" IS_VALUE_EMPTY="false" id="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.SEARCH_PATH.1097407993.f28335" name="Add &lt;dir&gt; to library search path (--search_path, -i)" superClass="com.ti.ccstudio.buildDefinitions.C2000_22.6.linkerID.SEARCH_PATH" valueType="libPaths">
<!-- Передаёт значение параметра: ${CG_TOOL_ROOT}/lib. -->
<listOptionValue builtIn="false" value="${CG_TOOL_ROOT}/lib" />
<listOptionValue builtIn="false" value="C:/ti/controlSUITE/libs/utilities/flash_api/2833x/28335/v210/lib" /> <!-- Находит установленную библиотеку TI без добавления её двоичного содержимого в проект. -->
<!-- Передаёт значение параметра: ${PROJECT_ROOT}. -->
<listOptionValue builtIn="false" value="${PROJECT_ROOT}" />
</option>
@@ -4,6 +4,7 @@ PAGE 0: /* Исполняемые секции и их Flash LOAD-адреса.
FLASHA : origin = 0x338000, length = 0x007F80 /* Сектор A до зарезервированной CSM-области содержит постоянный загрузчик. */
BEGIN : origin = 0x33FFF6, length = 0x000002 /* Два слова аппаратного Flash boot перехода; пароли CSM не выделяются. */
RAMFUNCS : origin = 0x00F800, length = 0x000800 /* Верхние 2048 слов RAM загрузчика нужны InitFlash и не входят в SCI8 payload приложения. */
RAMAPI : origin = 0x008000, length = 0x001800 /* 6144 слова временно принадлежат TI Flash API; приложение использует эту RAM только после выхода загрузчика. */
ADC_CAL : origin = 0x380080, length = 0x000009 /* Заводская калибровка TI используется без записи/стирания. */
RESET : origin = 0x3FFFC0, length = 0x000002 /* Аппаратный reset находится в ROM и не программируется. */
PAGE 1: /* Рабочие данные не пересекают код RAM-приложения ниже E000. */
@@ -15,6 +16,7 @@ PAGE 1: /* Рабочие данные не пересекают код RAM-пр
SECTIONS /* Не выделяет ни одного загружаемого слова в RAM приложения. */
{ /* Инициализированные Flash-секции остаются в секторе A. */
codestart : > BEGIN, PAGE = 0 /* Аппаратный переход передаёт управление watchdog-safe C startup. */
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 выполняются из RAM, пока общий банк Flash занят изменением B–H. */
.text : > FLASHA, PAGE = 0 /* Весь постоянный алгоритм и библиотеки исполняются из Flash A. */
.cinit : > FLASHA, PAGE = 0 /* Таблицы ROM-модели подготавливают данные загрузчика до main. */
.pinit : > FLASHA, PAGE = 0 /* Сохраняет стандартный порядок C runtime, если компилятор создаст эту секцию. */
@@ -0,0 +1,33 @@
#include "DSP2833x_Device.h" /* Предоставляет реальные типы C28x, частоту SYSCLKOUT, CSM и маскирование IRQ. */
#include "../../../ports/tms320f28335/f28335_flash_update.h" /* Подключает проверяемый алгоритм staging SCI8 и аппаратный контракт записи. */
#include "Flash2833x_API_Library.h" /* Использует официальный TI Flash API v2.10 для F28335, а не запись Flash обычным указателем. */
#include <string.h> /* Копирует код и таблицы TI API из постоянного сектора A во внутреннюю RAM до операции Flash. */
extern Uint16 FlashApiLoadStart, FlashApiLoadEnd, FlashApiRunStart; /* Символы LOAD/RUN задаёт linker в адресуемых словах CPU. */
static FLASH_ST flash_status; /* Диагностика TI хранится во внутренней RAM во время стирания и программирования. */
static bool erase_flash(void *user) /* Стирает только область приложения, сохраняя постоянный загрузчик. */
{ /* Даже IRQ из сектора A запрещены, поскольку F28335 имеет общий банк Flash для A–H. */
Uint16 saved, status; (void)user; /* Сохраняет прежнее состояние IRQ и точный результат TI API. */
saved = __disable_interrupts(); status = Flash_Erase(0x00FEU, &flash_status); __restore_interrupts(saved); /* Бит 0 — сектор A; маска FE выбирает только B–H согласно TI API v2.10. */
return status == STATUS_SUCCESS; /* Ошибка erase запрещает любую последующую публикацию FAB1. */
} /* После возврата TI API банк Flash снова доступен коду постоянного загрузчика. */
static bool program_flash(void *user, uint32_t address, uint16_t *data, unsigned int words) /* Записывает и проверяет одну ограниченную порцию Flash. */
{ /* Аппаратный слой повторно защищает сектор A независимо от валидатора SCI8. */
Uint16 saved, status; (void)user; /* Результат API и маска IRQ не размещаются в обновляемой Flash. */
if (!words || address < F28335_FLASH_APP_START || address >= F28335_FLASH_APP_STOP || words > F28335_FLASH_APP_STOP - address) return false; /* Не допускает даже частичного выхода за B–H, включая CSM и аппаратный boot. */
saved = __disable_interrupts(); status = Flash_Program((Uint16 *)address, data, words, &flash_status); /* Во время программирования процессор выполняет только скопированный в RAM TI API. */
if (!status) status = Flash_Verify((Uint16 *)address, data, words, &flash_status); /* Проверяет фактические слова до подтверждения этой порции. */
__restore_interrupts(saved); return status == STATUS_SUCCESS; /* Возвращает исходное состояние IRQ даже при отказе записи или readback. */
} /* Общий writer опубликует FAB1 лишь после успешной записи остальных порций. */
static uint16_t read_flash(void *user, uint32_t address) /* Читает физическую Flash для итоговой проверки FAB1/CRC. */
{ /* Этот callback вызывается только после окончания аппаратных операций TI API. */
(void)user; return *(volatile Uint16 *)address; /* Адрес уже ограничен общим валидатором диапазоном приложения B–H. */
} /* Программирование не подменяется чтением staging EEPROM. */
bool F28335Boot_FlashProgram(f28335_flash_update_t *s) /* Единственная точка фактической записи внутренней Flash из CAN COMMIT. */
{ /* Постоянный загрузчик может вернуться к CAN recovery при любой ошибке. */
f28335_flash_ops_t ops; /* Аппаратные операции передаются проверяемому транспортно-независимому writer. */
if (CsmRegs.CSMSCR.bit.SECURE) return false; /* Защищённый CSM запрещает стирание до любого вызова TI API. */
memcpy(&FlashApiRunStart, &FlashApiLoadStart, &FlashApiLoadEnd - &FlashApiLoadStart); /* API и его константы должны быть в RAM даже при сохранении сектора A. */
Flash_CPUScaleFactor = (Uint32)(1048576.0L * (200.0L * (long double)SYSCLKOUT / 1000000000.0L)); /* Формула TI IQ20 использует реальную частоту SYSCLKOUT в герцах. */
Flash_CallbackPtr = 0; ops.erase = erase_flash; ops.program = program_flash; ops.read = read_flash; ops.user = 0; /* Исключает callback во Flash во время занятости общего банка A–H. */
return f28335_flash_update_program(s, &ops); /* Итоговый ACK COMMIT появляется только после CRC физической Flash; REBOOT остаётся отдельной командой клиента. */
} /* Постоянный boot, handoff и EEPROM-параметры не программируются. */
+8 -1
View File
@@ -216,7 +216,7 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
pcan_boot_byte_t command = (pcan_boot_byte_t)(body & 0xFFU);
if (dlc != ((command == PCAN_BOOT_CMD_BEGIN_IMAGE || command == PCAN_BOOT_CMD_BEGIN_COMPAT) ? 8U :
(command == PCAN_BOOT_CMD_DEVICE_NAME || command == PCAN_BOOT_CMD_MEMORY_INFO) ? 1U : 0U)) {
(command == PCAN_BOOT_CMD_DEVICE_NAME || command == PCAN_BOOT_CMD_MEMORY_INFO || command == PCAN_BOOT_CMD_SELECT_MEMORY) ? 1U : 0U)) { /* Выбор памяти имеет ровно один октет и не может читать отсутствующий payload. */
return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_SIZE);
}
if (command == PCAN_BOOT_CMD_PING) {
@@ -269,6 +269,13 @@ static bool process_control(pcan_boot_t *boot, uint16_t body,
boot->last_activity_ms=boot->now_ms;
switch (command) {
case PCAN_BOOT_CMD_SELECT_MEMORY: { /* Явное назначение Flash не меняет поведение прежних клиентов без этой команды. */
pcan_boot_status_t status; /* Точный отказ порта передаётся клиенту без подмены успешным ACK. */
if (boot->state != PCAN_BOOT_STATE_METADATA || boot->have_image || boot->have_compat) return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_STATE); /* После начала метаданных, приёма либо проверки запрещает менять цель записи. */
if (!boot->port.select_memory) return send_status(boot, command, PCAN_BOOT_STATUS_INVALID_COMMAND); /* Старый загрузчик отклоняет новую цель до ERASE и передачи образа. */
status = boot->port.select_memory(boot->port_user, data[0], &boot->config); /* Порт проверяет поддержку выбранной физической памяти и задаёт её режим. */
return send_status(boot, command, status); /* Ответ защищён исходной адресацией и текущей ненулевой сессией. */
} /* Остальные команды используют выбранную конфигурацию слотов. */
case PCAN_BOOT_CMD_BEGIN_IMAGE:
if(boot->state==PCAN_BOOT_STATE_READY_TO_ERASE && boot->manifest.image_size==get_u32(data) &&
boot->manifest.image_crc32==get_u32(data+4)) return send_status(boot,command,PCAN_BOOT_STATUS_OK);
+1
View File
@@ -48,6 +48,7 @@ set(SETPROTOCOL_LEGACY_SOURCES
set(SETPROTOCOL_SOURCES
src/deif_xdi.c # Общее C99-ядро DEIF для всех платформ.
src/set_invertor_gas.c # Подключает две проекции одной карты STM Invertor: GAS 0x3 и масочный GAS SLOT 0x4.
src/set_deif_gauge.c
src/set_regmap.c
src/set_emu_ump.c
src/set_emu_tms.c
+10
View File
@@ -11,6 +11,16 @@
`include/balsam167_eeprom_layout.h`; PEEK, CRC и порядок байтов реализованы
в `src/periph28335.c`. Проверка полного диапазона — `test_periph28335_eeprom`.
Декодер `balsam_can` знает служебные слова heartbeat периферии Бальзам-167:
`0x0100` — счётчик секунд по модулю 65536, `0x0101` — аппаратный Mode,
`0x0102` — код типа платы Desk. Назначения соответствуют `CAN_HEARTBEAT_ADDR`
и ветке `CAN_send` в `Balsam_167_periph/Source/Internal/ecan.c`.
Python-адаптер `set_devices.balsam_can` выделяет ответ с адресом `0x0100`
и маской `111` как `BALZAM Heartbeat`, сохраняя исходные регистры и маршрут.
Пример: `0x00BA0017`, `00 08 00 04 E1 00 00 5A` — heartbeat задатчика,
90 секунд, Mode=8, Desk=4. Счётчик отображается без знака, в том числе
при значениях `0x8000–0xFFFF`; обычные технологические int16 сохраняют знак.
Универсальные сервисы эмуляторов: [адресное пространство, API и порт новой платы](docs/EMULATOR_SERVICES.md).
`set_regmap` объединяет УМП, 2812, IGBT и DAC за общим API для RTU, CAN и других оболочек.
+26
View File
@@ -1,5 +1,31 @@
# Универсальное адресное пространство эмуляторов
## Подключение панелей к конкретному MCU
Каталог `python/set_devices/mcu_profiles.py` связывает платы с общими сервисами,
моделью `DEVICE_INFO`, настройками подключения и аппаратным таймером.
`python/set_devices/qt_ports/emulator_workspace.py` принимает готовые панели
потребителя по ключам `settings`, `ump`, `tms2812`, `igbt`, `wavegen` и выбирает
их состав методом `select_mcu`. Общие алгоритмы УМП, 2812, IGBT и ЦАП остаются
в описанном ниже C-ядре; новая панель не создаёт вторую модель или соединение.
SETGUI подключает эти инструменты из страниц «Терминал → STM32 → MCU».
Поддержаны эмулятор STM32F407VET6 с TIM6 72 МГц и порт STM32G474RB с TIM6
160 МГц. Параметры COM хранятся отдельно; расчёт частот ЦАП, подписи Vin
и проверяемая модель SET v2 соответствуют выбранной плате. Профиль MCU
нельзя переключать во время обмена. Сам выбор профиля не отправляет команды.
Общий автомат обнаружения находится в `python/set_devices/emulator_protocol.py`,
COM-адаптер и клиент IGBT — в `python/set_devices/qt_ports/emulator_serial.py`
и `igbt_client.py`. Они не импортируют SETGUI. Старые пути SETGUI только
реэкспортируют эти реализации для совместимости.
STM32G474VET6 с прошивкой CANdle имеет отдельный профиль BOOT: приложение
`0x08008000`, размер до 480 КиБ, CAN 500 кбит/с, Type 7 / Device 15 /
Product 0, либо USART1 PA9/PA10 на 512000 бод, 8N1. Это другая прошивка,
чем эмулятор STM32G474RB. Профиль BOOT готовит форму обновления и проверяет
векторы приложения до начала CAN-передачи.
Общий код находится в `templates/c/set-protocol`. Прошивка платы задаёт память,
GPIO и аппаратные callbacks; протокол и модели не включают STM32/HAL/USB/ОС,
не выделяют heap и не читают системное время. Все вызовы одного экземпляра
+25
View File
@@ -0,0 +1,25 @@
#ifndef SET_DEIF_GAUGE_H /* Переносимая геометрия прибора подключается один раз. */
#define SET_DEIF_GAUGE_H /* Макет повторяет прибор SETGUI в логических пикселях 600×510. */
#include <stdint.h> /* Цвет передаётся как RGB888 независимо от framebuffer и Qt. */
#include "pcan_abi.h" /* Общий макрос экспорта делает графику доступной также из shared-библиотеки. */
#ifdef __cplusplus /* Qt/C++ использует те же немодифицированные имена функций C. */
extern "C" { /* Открывает переносимый ABI без привязки к конкретному GUI. */
#endif /* Начало совместимости с C++. */
typedef struct { /* Снимок содержит физические значения, а не wire-байты CAN. */
double value[6], angle, command[3]; /* Порядок value совпадает с deif_xdi.h; command: POWER%, RPM%, градусы. */
unsigned valid, angle_kind, commands, role, node, source; /* Маска свежести, тип угла, черновики и подписи роли/узлов; role=2 обозначает MASTER, остальные роли показывают принятые данные. */
} setp_deif_gauge_values; /* Выбор свежести и фактического угла выполняет общее протокольное ядро. */
typedef struct { /* Потребитель преобразует логические пиксели в свой холст. */
void *user; /* Непрозрачный контекст рисующего порта. */
void (*line)(void *,double,double,double,double,uint32_t,double); /* Отрезок x1/y1/x2/y2, RGB888 и толщина в пикселях эталона; порт сохраняет сглаживание при масштабировании. */
void (*rect)(void *,double,double,double,double,uint32_t); /* Прямоугольник x/y/w/h; ненулевой старший байт задаёт alpha, RGB888 без старшего байта означает непрозрачный цвет. */
void (*text)(void *,double,double,double,double,const char *,uint32_t,double,unsigned); /* UTF-8, RGB888, размер Qt в пунктах; style: 1 bold, 2 stretch80%, 4 left, 8 right. */
void (*triangle)(void *,const double *,uint32_t); /* Три пары логических координат задают заполненные указатели движителя без копирования геометрии в порт. */
void (*azimuth)(void *,unsigned); /* Значение 1 включает одинаковый масштаб X/Y вокруг центра компаса; 0 возвращает растяжение остальных элементов макета. */
} setp_deif_gauge_painter; /* Ни Qt, ни Linux, ни выделение памяти не входят в контракт. */
PCAN_ABI_API void setp_deif_gauge_draw(const setp_deif_gauge_values *v,const setp_deif_gauge_painter *p); /* Рисует шкалы и движитель; ноль азимута направлен вверх. */
PCAN_ABI_API int setp_deif_gauge_hit(double x,double y,double *value); /* Возвращает 1=POWER%, 2=RPM%, 3=угол либо 0 вне активных зон. */
#ifdef __cplusplus /* Закрывает единый ABI при включении заголовка из Qt/C++. */
} /* Конец extern C. */
#endif /* Чистая C99-сборка не получает лишних конструкций C++. */
#endif /* Конец общего интерфейса графики DEIF. */
+2 -1
View File
@@ -31,7 +31,8 @@ enum set_plot_operation {
PCAN_ABI_API uint32_t set_plot_abi_version(void);
/** Evaluates one operation. Returns output count, or 0 for invalid arguments.
* TRANSFORM rejects malformed viewports; invalid gesture values return the
* unchanged viewport. Zoom is restricted to 1..128, with focus anchoring.
* unchanged viewport. Zoom is restricted to 1..10000000 on X and 1..128 on Y,
* with focus anchoring.
* Screen fractions increase downwards; set inverted=1 for the Y value axis.
*/
PCAN_ABI_API size_t set_plot_eval(uint32_t operation, const double *input,
+10
View File
@@ -13,6 +13,7 @@ extern "C" {
#endif
enum { SET_SIGNAL_POLYNOMIAL, SET_SIGNAL_LINEAR, SET_SIGNAL_PCHIP, SET_SIGNAL_SPLINE };
/* Caller-owned workspace: at least 14*count+128 doubles. No heap or globals.
* Bounds: 2..100000 input points, 2..1000000 output points.
* Inputs may be unsorted; duplicate times are averaged. No extrapolation.
* meta[0..2] = original count, unique count, RMSE at original measurements.
* endpoint=1 includes the last time; 0 samples a cyclic period [first,last).
@@ -26,6 +27,15 @@ enum { SET_SIGNAL_POLYNOMIAL, SET_SIGNAL_LINEAR, SET_SIGNAL_PCHIP, SET_SIGNAL_SP
PCAN_ABI_API int set_signal_reconstruct(const double *x, const double *y, size_t count,
int method, unsigned degree, size_t output_count, int endpoint,
double *out_x, double *out_y, double *meta, double *work, size_t work_count);
/* Дополнительный решатель получает уникальные нормированные X/Y и пишет degree+1
* коэффициентов по возрастанию степени для аргумента 2*x-1. Код 0 означает успех,
* 3 — недостаточный ранг, 5 — ошибку памяти/решателя. NULL сохраняет прежний МНК.
* C99-подготовка, интерполяция, RMSE и размещение work одинаковы для всех решателей. */
typedef int (*set_signal_fit_fn)(const double *, const double *, size_t, unsigned, double *); /* Обратный вызов отделяет необязательный C++-порт от переносимого ядра. */
PCAN_ABI_API int set_signal_reconstruct_with_fit( /* Расширение ABI не меняет сигнатуру существующей функции для прошивок и GUI. */
const double *x, const double *y, size_t count, int method, unsigned degree, /* Входные точки и степень имеют те же ограничения, что у исходного API. */
size_t output_count, int endpoint, double *out_x, double *out_y, /* Результат остаётся в памяти вызывающего и сохраняет выбор конечной точки. */
double *meta, double *work, size_t work_count, set_signal_fit_fn fit); /* Решатель используется только для полинома; рабочая память сохраняет прежний формат. */
/* Reject out-of-range voltages, never silently clip. vref must be positive.
* All input is validated before any output is written. */
PCAN_ABI_API int set_signal_dac12(const double *volts, size_t count, double vref,
+4
View File
@@ -30,8 +30,12 @@ typedef struct {
uint16_t count, received, ready, running, rate_high;
uint32_t rate;
set_wave_port port;
uint32_t *xy_samples; /* Упакованные пары DHR12RD: X в битах 0…11, Y в 16…27; память предоставляет плата. */
int (*start_xy)(void *, const uint32_t *, size_t, uint32_t); /* Запускает обе координаты общим таймером и одним DMA без сдвига между каналами. */
uint16_t channels, selected, received_other; /* Режим 1/2, выбранная координата 0/1 и число отсчётов второй координаты для общего commit. */
} set_wave_state;
void set_wave_init(set_wave_state *, uint16_t *, size_t, const set_wave_port *);
void set_wave_enable_xy(set_wave_state *, uint32_t *, int (*)(void *, const uint32_t *, size_t, uint32_t)); /* Подключает необязательный аппаратный порт XY после обычной инициализации. */
/* Atomic sample-block transfer. values=NULL builds a read; otherwise a write.
* At most 120 words: request <=249 bytes, response <=245 bytes. */
PCAN_ABI_API size_t set_wave_block_request(unsigned device,unsigned index,
+1 -1
View File
@@ -33,6 +33,6 @@ LOCAL_SRC_FILES += ../../src/set_gas_watch.c # Включает общий GAS_W
LOCAL_SRC_FILES += ../../src/pcan_gas_slot.c gas_slots_jni.c # Android собирает общий слотный кодек и тонкую JNI-границу без второго алгоритма.
LOCAL_SRC_FILES += ../../src/set_invertor_gas.c # Android получает ту же C-реализацию нового профиля без копирования алгоритмов.
LOCAL_CFLAGS := -std=c99 -Wall -Wextra -Wpedantic -fvisibility=hidden
LOCAL_SRC_FILES += ../../src/deif_xdi.c set_deif_jni.c # Единственные DEIF-правила компилируются вместе с тонким JNI-портом.
LOCAL_LDLIBS := -llog -lm
LOCAL_SRC_FILES += ../../src/deif_xdi.c set_deif_jni.c # Общий DEIF и тонкий JNI без второй реализации.
include $(BUILD_SHARED_LIBRARY)
@@ -152,14 +152,17 @@ object BalsamCanProtocol {
fun deviceName(device: Int): String = if (NativeSetProtocol.available) {
NativeSetProtocol.nativeBalsamDeviceName(device)
} else listOf(
"Трансформатор 1", "Трансформатор 2", "Силовой блок 1", "Силовой блок 2",
"УМП 1", "УМП 2", "Двигатель", "ВЭП", "Задатчик", "Узел 10", "Узел 11",
"Трансформатор 1", "Трансформатор 2", "Силовой 1", "Силовой 2", // Имена узлов 1..4 повторяют таблицу адресов 167 и общее C-ядро.
"УМП 1", "УМП 2", "Двигатель", "Задатчик", "ВЭП", "ПМУ", "Узел 11", // Узлы 5..10 соответствуют таблице, включая ПМУ на паре 0xBA0009/0xBA0019; узел 11 остаётся служебным.
"Узел 12", "Терминал",
).getOrElse(device - 1) { "Неизвестный узел" }
fun registerName(device: Int, address: Int): String =
if (NativeSetProtocol.available) NativeSetProtocol.nativeBalsamRegisterName(device, address)
else when {
address == 0x0100 -> "Heartbeat: счётчик, с" // CAN_HEARTBEAT_ADDR из ecan.c содержит беззнаковый секундный счётчик по модулю 65536.
address == 0x0101 -> "Heartbeat: Mode" // Второе слово служебного пакета передаёт аппаратный номер Mode платы.
address == 0x0102 -> "Heartbeat: тип платы (Desk)" // Третье слово передаёт Desk, чтобы Android без JNI использовал названия общего C-ядра.
device in 1..2 && address in 0x18..0x2B -> "Показания T° ${address - 0x17}"
device in 3..4 && address in 0x18..0x27 -> "Показания T° ${address - 0x17}"
device == 7 && address in 0x18..0x1F -> "Показания T° ${address - 0x17}"
+17 -16
View File
@@ -2,7 +2,7 @@
#include "board_config.h" /* Выбирает размещение Flash, USART1 и разрешение физических выводов IGBT. */
#include "transport.h" /* Объявляет запуск общего UART/FDCAN-транспорта после настройки GPIO. */
#include "emulator.h" /* Подключает единый шаг моделей УМП, 2812, IGBT и генератора. */
#include "igbt_realtime_port.h" /* Объявляет аппаратный IGBT версии 4 с наносекундными compare. */
#include "igbt_realtime_port.h" /* Объявляет аппаратный IGBT версии 4 без миллисекундного обслуживания фронтов. */
#include "emulator_port.h" /* Даёт доступ к существующей карте сервера до первого транспортного запроса. */
#include "igbt_emulator.h" /* Передаёт физическую маску Vin и получает маску ответов Vstat. */
#include "stm32g4xx_hal.h" /* Предоставляет HAL RCC/PWR/GPIO и определения регистров Cortex-M4. */
@@ -11,8 +11,8 @@ uint32_t SystemCoreClock = 16000000u; /* После reset HSI16 работает
const uint8_t AHBPrescTable[16] = {0,0,0,0,0,0,0,0,1,2,3,4,6,7,8,9}; /* CMSIS-кодирует делители HPRE как степени двойки по RM0440. */
const uint8_t APBPrescTable[8] = {0,0,0,0,1,2,3,4}; /* HAL использует кодировку PPRE для вычисления частоты периферии G4. */
static uint8_t usb_clock_ready; /* До настройки HSI48/CRS предотвращает преждевременный запуск USB. */
#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Не занимает выводы FLASH/CAN2/CAN3 на неподтверждённой ревизии HOPE по умолчанию. */
static const uint16_t input_pins[8] = {GPIO_PIN_0,GPIO_PIN_1,GPIO_PIN_2,GPIO_PIN_10,GPIO_PIN_11,GPIO_PIN_12,GPIO_PIN_13,GPIO_PIN_14}; /* Ручной профиль Vin PB0/1/2/10..14 совпадает с прежним портом G474. */
#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Штатный профиль использует свободные линии J5/J6 по схеме HOPE от 14.04.2025, сохраняя все три CAN. */
static const uint16_t input_pins[8] = {GPIO_PIN_0,GPIO_PIN_1,GPIO_PIN_2,GPIO_PIN_10,GPIO_PIN_11,GPIO_PIN_6,GPIO_PIN_7,GPIO_PIN_14}; /* Vin0…7: J5-13/14/15/22/23, J6-7/8, J5-24; PB6/PB7 заменяют занятые CAN2 линии PB12/PB13. */
#endif /* Без физического порта все команды IGBT остаются доступны через общую карту регистров. */
void SystemInit(void) /* Startup вызывает функцию до инициализации глобальных переменных C runtime. */
{ /* До runtime доступны только константы сборки и регистры, поэтому HAL здесь не вызывается. */
@@ -68,7 +68,7 @@ static void peripheral_clock(void) /* Создаёт одинаковую час
if (HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_4) != HAL_OK) board_fatal(3); /* Четыре wait-state соответствуют 160 МГц в Boost Range1 по RM0440. */
peripheral.PeriphClockSelection = RCC_PERIPHCLK_USB | RCC_PERIPHCLK_FDCAN | RCC_PERIPHCLK_USART1; /* Выбирает только реальные блоки HOPE, исключая USART2 профиля Nucleo. */
peripheral.UsbClockSelection = RCC_USBCLKSOURCE_HSI48; /* Подаёт независимые 48 МГц на USB FS. */
peripheral.FdcanClockSelection = RCC_FDCANCLKSOURCE_PCLK1; /* Общий CAN-порт рассчитывает 250 кбит/с из фактических 80 МГц PCLK1. */
peripheral.FdcanClockSelection = RCC_FDCANCLKSOURCE_PCLK1; /* Подаёт PCLK1 80 МГц на FDCAN; общий порт получает 125000 бит/с из EMU_CAN_BITRATE при 16 квантах на бит и делителе 40. */
peripheral.Usart1ClockSelection = RCC_USART1CLKSOURCE_PCLK2; /* HAL вычисляет делитель USART1 из 160 МГц APB2. */
if (HAL_RCCEx_PeriphCLKConfig(&peripheral) != HAL_OK) board_fatal(3); /* Ошибка выбора kernel clock обнаруживается до включения транспорта. */
__HAL_RCC_CRS_CLK_ENABLE(); /* Разрешает блок автоматической подстройки HSI48 по USB SOF. */
@@ -91,9 +91,10 @@ void board_init(void) /* Вызывается после создания общ
HAL_GPIO_Init(GPIOA, &gpio); /* Применяет HAL-настройку UART к порту A одним вызовом. */
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1; gpio.Alternate = GPIO_AF9_FDCAN1; /* PD0 RX и PD1 TX соединены с U6 TJA1051T/3; PB8 на этой плате занят BOOT0. */
HAL_GPIO_Init(GPIOD, &gpio); /* Подключает FDCAN1 к штатным CAN1L/CAN1H разъёма J4 по схеме от 14.04.2025. */
#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Физический профиль включается вручную после проверки занятых выводов FLASH и других разъёмов. */
#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Подключает физический IGBT через свободные выводы, не затрагивая CAN1/2/3, USB, UART, DAC и дисплей. */
__HAL_RCC_GPIOC_CLK_ENABLE(); /* Порт C нужен только для восьми физических выходов Vstat. */
gpio.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14; /* Сохраняет отключённый ручной профиль Vin; PB12/PB13 соединены с CAN2 и требуют изменения внешних соединений. */
gpio.Pin = 0; /* Начинает общую маску Vin с пустого набора, чтобы инициализация и опрос использовали одну таблицу каналов. */
for (unsigned i = 0; i < 8; ++i) gpio.Pin |= input_pins[i]; /* Объединяет восемь свободных выводов GPIOB; индексы массива задают биты Vin0…7. */
gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_PULLDOWN; HAL_GPIO_Init(GPIOB, &gpio); /* Задаёт нулевой Vin на неподключённых входах выбранного ручного профиля. */
gpio.Pin = 0xFFu; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; /* Биты 0..7 порта C становятся восемью ответами Vstat IGBT. */
HAL_GPIO_WritePin(GPIOC, 0xFFu, GPIO_PIN_RESET); HAL_GPIO_Init(GPIOC, &gpio); /* Сначала записывает нули в ODR, исключая импульс при включении выходов. */
@@ -102,23 +103,23 @@ void board_init(void) /* Вызывается после создания общ
gpio.Pin = SERIAL_DE_PIN; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; /* Настраивает выбранный внешний вывод DE, не меняя UART TX/RX. */
HAL_GPIO_WritePin(SERIAL_DE_PORT, SERIAL_DE_PIN, GPIO_PIN_RESET); HAL_GPIO_Init(SERIAL_DE_PORT, &gpio); /* Начинает в режиме приёма с выключенным передатчиком RS-485. */
#endif /* Штатный TTL UART не занимает вывод DE. */
#if EMU_IGBT_REALTIME /* Регистрирует аппаратный сервис только в новой сборке HOPE. */
hope_igbt_realtime_init(emu_server_state()); /* Подменяет окно 0x1400 до первого запроса, сохраняя остальные модели и транспорты. */
#endif /* Прежний профиль продолжает использовать миллисекундный IGBT версии 3. */
#if EMU_IGBT_REALTIME /* Регистрирует наносекундный сервис только для явно выбранной аппаратной сборки HOPE. */
hope_igbt_realtime_init(emu_server_state()); /* Подменяет окно 0x1400 до запуска UART/CAN, сохраняя остальные сервисы сервера. */
#endif /* Прежний профиль продолжает использовать окно миллисекундной модели версии 3. */
transport_init(); /* Запускает HAL UART IRQ и Classic CAN после полной настройки GPIO/clock. */
} /* Завершает аппаратный запуск HOPE, не запуская генерацию DAC без команды. */
uint8_t board_usb_ready(void) { return usb_clock_ready; } /* Возвращает main готовность источника USB, общую для обоих вариантов Flash. */
void board_tick(void) /* Выполняется главным циклом один раз на каждую обслуженную миллисекунду. */
{ /* Протокольная инъекция IGBT работает и при отключённом физическом GPIO. */
#if EMU_IGBT_REALTIME /* Выходные сроки обслуживаются аппаратными таймерами независимо от миллисекундного цикла. */
emu_tick(); /* Сохраняет штатное время моделей и применения выбранного режима MCU. */
hope_igbt_realtime_poll(); /* Останавливает аппаратный IGBT в том же шаге, в котором выбран другой режим. */
#else /* Ветка совместимости сохраняет прежние миллисекундные GPIO. */
#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Считывает восемь физических Vin только для явно включённого ручного профиля. */
#if EMU_IGBT_REALTIME /* Аппаратные таймеры обслуживают все фронты независимо от этого шага. */
emu_tick(); /* Сохраняет миллисекундное время УМП, 2812, DAC, DEIF и применения выбора режима. */
hope_igbt_realtime_poll(); /* Выключает аппаратные выходы в том же шаге, в котором выбран другой режим платы. */
#else /* Сборка совместимости сохраняет прежний программный IGBT с шагом 1 мс. */
#if EMU_IGBT_GPIO_ENABLE && !EMU_IGBT_REALTIME /* Считывает восемь физических Vin штатного профиля HOPE с PB6/PB7 вместо CAN2. */
uint8_t inputs = 0, outputs; unsigned i; /* Накопленная маска привязана к порядку input_pins, а не к номерам PB. */
for (i = 0; i < 8; ++i) if (HAL_GPIO_ReadPin(GPIOB, input_pins[i]) == GPIO_PIN_SET) inputs |= (uint8_t)(1u << i); /* Переводит восемь уровней в логические Vin0..Vin7. */
igbt_emu_set_tx(inputs); /* Передаёт физическую маску общей модели, сохраняя её выбор источника через регистр 0x140C. */
#else /* Штатный профиль HOPE не использует неподтверждённые внешние дискретные линии. */
#else /* Явное отключение GPIO оставляет модель доступной через регистровый источник TX. */
igbt_emu_set_tx(0); /* При выборе GPIO задаёт нулевой Vin; регистровый источник остаётся независимым. */
#endif /* Далее все модели продвигаются одинаково независимо от наличия дискретного порта. */
emu_tick(); /* Продвигает выбранную модель и общие отложенные настройки ровно на 1 мс. */
@@ -127,5 +128,5 @@ void board_tick(void) /* Выполняется главным циклом од
HAL_GPIO_WritePin(GPIOC, (uint16_t)(~outputs & 0xFFu), GPIO_PIN_RESET); /* Сбрасывает только неактивные PC0..PC7 без изменения остальных выводов C. */
HAL_GPIO_WritePin(GPIOC, outputs, GPIO_PIN_SET); /* Устанавливает активные ответы Vstat текущего шага модели. */
#endif /* Завершает опциональное обновление физических ответов. */
#endif /* Завершает выбор аппаратного либо прежнего логического IGBT. */
#endif /* Завершает выбор между аппаратным IGBT и прежней миллисекундной моделью. */
} /* Завершает один миллисекундный шаг без обработки моделей из прерываний. */
@@ -1,7 +1,7 @@
#ifndef HOPE_BOARD_CONFIG_H /* Защищает единый профиль платы HOPE от повторного включения. */
#ifndef EMU_IGBT_REALTIME /* Разрешает явную сборку прежней логической модели для старого GUI. */
#define EMU_IGBT_REALTIME 1 /* Штатный HOPE использует HRTIM D/E/F и TIM2/3/4/5/15 с окном IGBT версии 4 в наносекундах. */
#endif /* Сохраняет явное переопределение параметром компилятора. */
#ifndef EMU_IGBT_REALTIME /* Позволяет явно собрать прежнюю миллисекундную модель для совместимости со старым GUI. */
#define EMU_IGBT_REALTIME 1 /* Штатный HOPE использует аппаратные HRTIM D/E/F и TIM2/3/4/5/15 с окном IGBT версии 4 в наносекундах. */
#endif /* Сохраняет явное переопределение аппаратного режима параметром сборки. */
#define HOPE_BOARD_CONFIG_H /* Используется всеми аппаратными адаптерами и общей композицией моделей. */
#define EMU_USB_SET_V2 1 /* Нативный USB MCU переносит SET v2; UART сохраняет RTU и SETGUI v1/GAS. */
#define USB_CDC_IRQn USB_LP_IRQn /* G474 обслуживает USB FS через низкоприоритетный вектор USB_LP. */
@@ -13,7 +13,7 @@
#define EMU_TMS_ADDRESS 1u /* Отдельный адрес 2812 позволяет отличать CMD_STD от команд настройки эмулятора. */
#define EMU_CAN_MODE 5u /* Общий протокол эмулятора использует extended RX 0xBA0004 и TX 0xBA0024. */
#define EMU_UART_BAUD 115200u /* Начальная скорость приложения в бодах; загрузчик CANdle отдельно использует 512000. */
#define EMU_CAN_BITRATE 250000u /* Начальная скорость приложения в бит/с; CAN-загрузчик отдельно использует 500000. */
#define EMU_CAN_BITRATE 125000u /* Скорость CAN приложения HOPE и DEIF по требованию пользователя: 125000 бит/с; при PCLK1 80 МГц и 16 квантах на бит делитель равен 40. */
#define EMU_FLAGS_DEFAULT 1u /* После сброса разрешает синтетический сценарий УМП; модель по умолчанию остаётся УМП. */
#define EMU_UART_INSTANCE USART1 /* Использует USART1 PA9/PA10, подключённый через JP2 к CH340N по схеме HOPE от 14.04.2025. */
#define EMU_UART_IRQn USART1_IRQn /* Направляет RX/TX HAL в аппаратный вектор USART1 вместо USART2 Nucleo. */
@@ -28,9 +28,9 @@
#define EMU_HSE_HZ 8000000u /* Номинал JT1 равен 8 МГц по схеме; HAL и PLL используют одну частоту в герцах. */
#endif /* Сохраняет явно переданную частоту внешнего источника. */
#define EMU_HSE_BYPASS 0 /* Внешний режим рассчитан на кварц; для готового генератора нужно отдельно выбрать bypass. */
#ifndef EMU_IGBT_GPIO_ENABLE /* Ручной профиль Vin требует устранить подтверждённое схемой пересечение PB12/PB13 с CAN2. */
#define EMU_IGBT_GPIO_ENABLE 0 /* Модель IGBT доступна через регистры; штатные линии CAN2 не становятся дискретными Vin. */
#endif /* Позволяет включить физические Vin/Vstat параметром компилятора. */
#ifndef EMU_IGBT_GPIO_ENABLE /* Позволяет отключить физический IGBT параметром компилятора при использовании только регистрового TX. */
#define EMU_IGBT_GPIO_ENABLE 1 /* Включает свободные Vin PB0/1/2/10/11/6/7/14 и Vstat PC0…PC7; CAN2 сохраняет PB12/PB13 по схеме HOPE. */
#endif /* Сохраняет явный выбор потребителя поверх штатного физического профиля. */
#ifndef EMU_FLASH_BASE /* Адрес задаётся одинаково в профиле Keil и его scatter-файле. */
#define EMU_FLASH_BASE 0x08008000u /* Резервирует первые 32 КиБ под найденный CAN/UART-загрузчик CANdle. */
#endif /* Самостоятельный профиль переопределяет адрес на 0x08000000. */
+17 -10
View File
@@ -61,7 +61,7 @@ int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
relative = can_id - BALSAM_CAN_BASE_ID;
output->direction = relative >= BALSAM_CAN_DATA_OFFSET
? BALSAM_CAN_FROM_NODE : BALSAM_CAN_TO_NODE;
output->device = (uint8_t)((relative & 0x0FU) + 1U);
output->device = (uint8_t)((relative & 0x0FU) + 1U); /* В ecan.c перед сборкой CAN ID выполняется DevNum--; маска 0x0F выделяет индекс 0..12, а +1 восстанавливает номер Mode. */
if (can_id == 0x00745009UL || can_id == 0x00745019UL) {
output->device = 10U;
output->direction = can_id == 0x00745019UL
@@ -80,9 +80,9 @@ int balsam_can_decode(uint32_t can_id, const uint8_t *data, size_t size,
const char *balsam_can_device_name(uint8_t device)
{
static const char *const names[BALSAM_CAN_NODE_COUNT] = {
"Трансформатор 1", "Трансформатор 2", "Силовой блок 1",
"Силовой блок 2", "УМП 1", "УМП 2", "Двигатель", "ВЭП",
"Задатчик", "Пульт (МПУ)", "Узел 11", "Узел 12", "Терминал"
"Трансформатор 1", "Трансформатор 2", "Силовой 1", /* Первые три узла соответствуют строкам 1..3 предоставленной таблицы адресов 167. */
"Силовой 2", "УМП 1", "УМП 2", "Двигатель", "Задатчик", /* Строки 4..8 таблицы; VPU1_CAN_DEVICE=8 соответствует задатчику, а не ВЭП. */
"ВЭП", "ПМУ", "Узел 11", "Узел 12", "Терминал" /* Строка 9 — ВЭП, пара 0xBA0009/0xBA0019 — ПМУ; служебные узлы 11..13 сохраняют прежние назначения. */
};
return (device >= 1U && device <= BALSAM_CAN_NODE_COUNT)
? names[device - 1U] : "Неизвестный узел";
@@ -91,6 +91,13 @@ const char *balsam_can_device_name(uint8_t device)
size_t balsam_can_register_name(uint8_t device, uint16_t address,
char *output, size_t output_size)
{
if (address == 0x0100U) /* CAN_HEARTBEAT_ADDR в ecan.c периферии 167 задаёт служебный адрес вне технологической карты R0..R127. */
return copy_name("Heartbeat: счётчик, с", output, output_size); /* Первое слово hiword содержит секунды после запуска по модулю 65536. */
if (address == 0x0101U) /* Следующее слово heartbeat соответствует старшим 16 битам loword в CAN_send. */
return copy_name("Heartbeat: Mode", output, output_size); /* Прошивка передаёт аппаратный Mode без уменьшения на единицу, применяемого только к CAN ID. */
if (address == 0x0102U) /* Третье слово heartbeat соответствует младшим 16 битам loword. */
return copy_name("Heartbeat: тип платы (Desk)", output, output_size); /* Значение Desk задаёт тип платы и отличается от номера Mode и номера CAN-узла. */
if ((device == 1U || device == 2U) && address < 20U)
return indexed_name("Диагностика T°", address + 1U, output, output_size);
if ((device == 1U || device == 2U) && address >= 0x18U && address <= 0x2BU)
@@ -139,7 +146,7 @@ size_t balsam_can_register_name(uint8_t device, uint16_t address,
if (device == 7U && address >= 0x18U && address <= 0x1FU)
return indexed_name("Показания T°", address - 0x17U, output, output_size);
if (device == 8U) {
if (device == 9U) { /* Карта напряжений принадлежит ВЭП с adr_SHKF=9; задатчик с номером 8 использует другую карту. */
static const char *const vep_names[] = {
"Диагностика 380В Ф1", "Диагностика 380В Ф2",
"Диагностика 31В Ф1", "Диагностика 31В Ф2",
@@ -164,15 +171,15 @@ size_t balsam_can_register_name(uint8_t device, uint16_t address,
return copy_name("Дискретные датчики (14 бит)", output, output_size);
}
if (device == 9U && address == 0U)
if (device == 8U && address == 0U) /* У VPU1_CAN_DEVICE=8 слово 0 передаёт обороты ГЭД, а не диагностику ВЭП. */
return copy_name("Обороты ГЭД, об/мин", output, output_size);
if (device == 9U && address == 1U)
if (device == 8U && address == 1U) /* Слово 1 той же платы задатчика содержит обороты гребного вала. */
return copy_name("Обороты ГВ, об/мин", output, output_size);
if (device == 9U && address == 2U)
if (device == 8U && address == 2U) /* ADR_CAN_LAMPS_ON_VPU=2 задаёт регистр первой группы ламп задатчика. */
return copy_name("Лампы (8 бит)", output, output_size);
if (device == 9U && address == 3U)
if (device == 8U && address == 3U) /* ADR_CAN_LAMPS_2_ON_VPU=3 задаёт регистр второй группы индикаторов задатчика. */
return copy_name("Диоды (16 бит)", output, output_size);
if (device == 9U && address == 0x10U)
if (device == 8U && address == 0x10U) /* ADR_CAN_KEY_ON_VPU=16, то есть 0x10, задаёт слово кнопок VPU; адрес не перенумеровывается. */
return copy_name("Кнопки (12 бит)", output, output_size);
if (address == 0x16U)
File diff suppressed because one or more lines are too long
+6 -4
View File
@@ -7,6 +7,8 @@
#include "set_plot.h"
#include <math.h>
#define MIN_VIEW_SPAN 1e-12 /* Минимальная безразмерная доля полного диапазона по X и Y задаёт увеличение до 10^12 раз; запас относительно точности double сохраняет различимые границы нормированной области. */
static double clamp(double v, double lo, double hi) { return fmin(hi, fmax(lo, v)); }
static int finite_values(const double *v, size_t n) {
size_t i;
@@ -23,13 +25,13 @@ size_t set_plot_eval(uint32_t op, const double *v, size_t n, double *out, size_t
cap < (op == SET_PLOT_TRANSFORM || op == SET_PLOT_LIMITS ? 4U : 1U)) return 0;
if (op == SET_PLOT_TRANSFORM) {
double w, h, fx, fy;
if (!finite_values(v, 4) || v[2] < 1.0/128 || v[2] > 1 ||
v[3] < 1.0/128 || v[3] > 1 || v[0] < 0 || v[1] < 0 ||
if (!finite_values(v, 4) || v[2] < MIN_VIEW_SPAN || v[2] > 1 || /* Проверяем конечность смещений и размеров, разрешая ширину до триллионной доли исходного диапазона X. */
v[3] < MIN_VIEW_SPAN || v[3] > 1 || v[0] < 0 || v[1] < 0 || /* Такой же предел для Y сохраняет возможность дальнейшего перемещения и увеличения уже приближённой области. */
v[0] + v[2] > 1.000000000001 || v[1] + v[3] > 1.000000000001) return 0;
out[0] = v[0]; out[1] = v[1]; out[2] = v[2]; out[3] = v[3];
if (!finite_values(v + 4, 6) || v[4] <= 0 || v[5] <= 0) return 4;
w = clamp(v[2] / v[4], 1.0/128, 1);
h = clamp(v[3] / v[5], 1.0/128, 1);
w = clamp(v[2] / v[4], MIN_VIEW_SPAN, 1); /* Делим текущую долю X на положительный коэффициент приближения; ограничиваем результат между новым пределом и полным диапазоном. */
h = clamp(v[3] / v[5], MIN_VIEW_SPAN, 1); /* Аналогично масштабируем Y, снимая прежнее ограничение 128 раз и сохраняя ненулевую высоту для координатных преобразований. */
fx = clamp(v[8], 0, 1); fy = clamp(v[9], 0, 1);
out[0] = clamp(v[0] + (v[2] - w) * fx - v[6] * w, 0, 1 - w);
out[1] = clamp(v[1] + (v[3] - h) * fy - v[7] * h, 0, 1 - h);
+14 -4
View File
@@ -33,14 +33,14 @@ static double evaluate(double t,const double *x,const double *y,size_t n,
return (2*u*u*u-3*u*u+1)*y[i]+(u*u*u-2*u*u+u)*h*d[i]
+(-2*u*u*u+3*u*u)*y[i+1]+(u*u*u-u*u)*h*d[i+1];
}
int set_signal_reconstruct(const double *input_x,const double *input_y,size_t count,
int method,unsigned degree,size_t output_count,int endpoint,double *out_x,
double *out_y,double *meta,double *work,size_t work_count) {
int set_signal_reconstruct_with_fit(const double *input_x,const double *input_y,size_t count, /* Общая подготовка данных обслуживает исходный и подключаемый решатели. */
int method,unsigned degree,size_t output_count,int endpoint,double *out_x, /* Метод, степень и временная сетка сохраняют контракт существующего API. */
double *out_y,double *meta,double *work,size_t work_count,set_signal_fit_fn fit) { /* NULL выбирает прежний МНК; другой callback получает уже нормированные данные. */
point *pairs; double *x,*y,*d,*h,*s,*q,*column;
double coef[6]={0},r[6][6]={{0}},origin,span,scale=1,rmse=0;
size_t i,j,k,n=0; unsigned pass;
if(!input_x||!input_y||!out_x||!out_y||!meta||!work || count<2||count>100000
||output_count<2||output_count>10000||work_count<14*count+128
||output_count<2||output_count>1000000||work_count<14*count+128
||method<0||method>3||(endpoint!=0&&endpoint!=1)) return 1;
pairs=(point *)work; x=work+2*count; y=x+count; d=y+count; h=d+count;
s=h+count; q=s+count; column=q+6*count;
@@ -65,6 +65,10 @@ int set_signal_reconstruct(const double *input_x,const double *input_y,size_t co
}
if(method==SET_SIGNAL_POLYNOMIAL) {
if(degree<1||degree>5||n<=degree) return 1;
if(fit) { /* Подключаемый решатель заменяет только вычисление коэффициентов МНК. */
int status=fit(x,y,n,degree,coef); /* Передаём уникальные нормированные точки и массив до шести коэффициентов. */
if(status) return status; /* Ошибка ранга или памяти возвращается явно, без скрытого перехода на другой решатель. */
} else { /* Исходный алгоритм остаётся неизменным вариантом для сравнения и прошивок. */
for(j=0;j<=degree;++j) {
double norm=0;
for(i=0;i<n;++i) column[i]=pow(2*x[i]-1,(double)j);
@@ -81,6 +85,7 @@ int set_signal_reconstruct(const double *input_x,const double *input_y,size_t co
j=i-1; for(k=j+1;k<=degree;++k) coef[j]-=r[j][k]*coef[k];
coef[j]/=r[j][j];
}
} /* Оба решателя возвращают коэффициенты в одном базисе для общего evaluate. */
} else if(method==SET_SIGNAL_PCHIP) {
d[0]=d[n-1]=s[0];
if(n>2) {
@@ -112,6 +117,11 @@ int set_signal_reconstruct(const double *input_x,const double *input_y,size_t co
meta[0]=(double)count; meta[1]=(double)n; meta[2]=sqrt(rmse)*scale;
return isfinite(meta[2])?0:2;
}
int set_signal_reconstruct(const double *input_x,const double *input_y,size_t count, /* Сохраняем прежний экспорт и поведение всех существующих клиентов. */
int method,unsigned degree,size_t output_count,int endpoint,double *out_x, /* Параметры передаются без изменения единиц времени и амплитуды. */
double *out_y,double *meta,double *work,size_t work_count) { /* Владение выходами и рабочим буфером по-прежнему остаётся у вызывающего. */
return set_signal_reconstruct_with_fit(input_x,input_y,count,method,degree,output_count,endpoint,out_x,out_y,meta,work,work_count,NULL); /* NULL выбирает исходный двухпроходный МНК без зависимости от C++. */
} /* Завершаем совместимую обёртку исходного C ABI. */
int set_signal_dac12(const double *volts,size_t count,double vref,uint16_t *codes) {
size_t i;
if(!volts||!codes||count<2||count>10000||!isfinite(vref)||vref<=0) return 1;
+51 -23
View File
@@ -13,18 +13,41 @@ static size_t seal(uint8_t *p,size_t n) {
uint16_t crc=set_crc16_modbus(p,n);p[n]=(uint8_t)crc;p[n+1]=(uint8_t)(crc>>8);return n+2;
}
void set_wave_init(set_wave_state *s,uint16_t *samples,size_t capacity,const set_wave_port *port) {
memset(s,0,sizeof(*s));s->samples=samples;s->capacity=capacity;s->rate=1000;
memset(s,0,sizeof(*s));s->samples=samples;s->capacity=capacity;s->rate=1000;s->channels=1; /* После reset сохраняет совместимость с прежним одноканальным клиентом. */
if(port) s->port=*port;
}
void set_wave_enable_xy(set_wave_state *s,uint32_t *pairs,int (*start)(void *,const uint32_t *,size_t,uint32_t)) { /* Плата явно объявляет доступность второго выхода и его SRAM. */
s->xy_samples=pairs;s->start_xy=start; /* Нулевой callback оставляет устройство одноканальным, включая прежние F103 и G474RB. */
} /* Подключение порта не запускает периферию и не меняет таблицу. */
static uint16_t sample_get(const set_wave_state *s,unsigned index) { /* Читает выбранную координату без перестановки готовых пар перед DMA. */
return s->channels==2?(uint16_t)((s->xy_samples[index]>>(16u*s->selected))&4095u):s->samples[index]; /* Полуслова DHR12RD разделены 16 битами; код каждого DAC занимает 12 бит. */
} /* Одноканальный формат uint16_t остаётся прежним. */
static void sample_set(set_wave_state *s,unsigned index,uint16_t value) { /* Изменяет только выбранную координату, сохраняя парный отсчёт другого канала. */
if(s->channels==2) { /* Двухканальная таблица уже хранится в формате единственной 32-битной передачи DMA. */
unsigned shift=16u*s->selected; /* Сдвиг 0 выбирает X/PA4, сдвиг 16 выбирает Y/PA5 по DHR12RD из RM0090/RM0440. */
s->xy_samples[index]=(s->xy_samples[index]&~(0xffffu<<shift))|((uint32_t)value<<shift); /* Очищает полуслово вместе с резервными битами и записывает проверенный 12-битный код. */
} else s->samples[index]=value; /* Сохраняет прежнюю компактную таблицу одиночного канала. */
} /* Проверки диапазона выполняет вызывающий обработчик до изменения памяти. */
static unsigned write_register(set_wave_state *s,unsigned reg,unsigned value) {
if(reg==SET_WAVE_BASE+2 && value==0) {
if(s->port.stop) s->port.stop(s->port.context);
s->running=0; return 0;
}
if(reg==SET_WAVE_BASE+10) { /* Выбор X/Y меняет только окно загрузки и чтения, поэтому доступен и при воспроизведении. */
uint16_t previous; /* Временно сохраняет счётчик прежней координаты при переключении окна. */
if(value>=s->channels)return 3; /* Одноканальная плата отвергает Y; произвольные индексы не меняют состояние. */
if(value!=s->selected) {previous=s->received;s->received=s->received_other;s->received_other=previous;s->selected=(uint16_t)value;} /* Меняет местами счётчики, сохраняя оба независимо от порядка загрузки. */
return 0; /* Выбор окна не снимает commit и не меняет фазу аппаратного вывода. */
} /* Остальные изменения таблицы запрещены во время running. */
if(s->running) return 6;
if(reg==SET_WAVE_BASE+9) { /* Число одновременно воспроизводимых каналов задаётся до начала новой загрузки. */
if(value<1||value>2||(value==2&&(!s->xy_samples||!s->start_xy)))return 3; /* Двойной режим допустим только с буфером пар и аппаратным исполнителем. */
s->channels=(uint16_t)value;s->selected=0;s->count=s->received=s->received_other=s->ready=0; /* Смена формата инвалидирует обе координаты, исключая запуск смешанной старой таблицы. */
return 0; /* Пуск остаётся отдельной командой после полного commit. */
} /* Прежние регистры частоты и длины сохраняют адреса. */
if(reg==SET_WAVE_BASE+2 && value==1) {
if(!s->ready||!s->port.start) return 3;
if(s->port.start(s->port.context,s->samples,s->count,s->rate)) return 4;
if(s->channels==2?s->start_xy(s->port.context,s->xy_samples,s->count,s->rate):s->port.start(s->port.context,s->samples,s->count,s->rate)) return 4; /* XY запускается одной аппаратной транзакцией; ошибка порта не выставляет running. */
s->running=1;return 0;
}
if(reg==SET_WAVE_BASE+3) {
@@ -38,37 +61,38 @@ static unsigned write_register(set_wave_state *s,unsigned reg,unsigned value) {
}
if(reg==SET_WAVE_BASE+4) {
if(value<2||value>SET_WAVE_MAX||value>s->capacity) return 3;
s->count=(uint16_t)value;s->received=s->ready=0;return 0;
s->count=(uint16_t)value;s->received=s->received_other=s->ready=0; /* Новая длина начинает загрузку обеих координат с нуля. */return 0;
}
if(reg>=SET_WAVE_DATA&&reg<SET_WAVE_DATA+SET_WAVE_MAX) {
unsigned index=reg-SET_WAVE_DATA;
if(index>=s->count||index>s->received||value>4095||s->ready) return 3;
if(index<s->received) return s->samples[index]==value?0:3;
s->samples[index]=(uint16_t)value;++s->received;return 0;
if(index<s->received) return sample_get(s,index)==value?0:3; /* Повтор подтверждается только при совпадении выбранной координаты; соседнее полуслово пары не учитывается. */
sample_set(s,index,(uint16_t)value); /* Готовит 16-битный отсчёт либо соответствующее полуслово XY. */++s->received;return 0;
}
if(reg==SET_WAVE_BASE+6) {
if(value!=0xA55A||s->count<2||s->received!=s->count) return 3;
if(value!=0xA55A||s->count<2||s->received!=s->count||(s->channels==2&&s->received_other!=s->count)) return 3; /* Общий commit требует полные X и Y одинаковой длины. */
s->ready=1;return 0;
}
return 2;
}
unsigned set_wave_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out) {
set_wave_state *s=(set_wave_state *)ctx;uint16_t w[9];unsigned i;
if(!s||!s->samples||!out||!count||(uint32_t)off+count>9)return 2;
set_wave_state *s=(set_wave_state *)ctx;uint16_t w[13]; /* Девять прежних слов дополнены режимом, селектором, возможностями и вторым счётчиком XY. */unsigned i;
if(!s||!s->samples||!out||!count||(uint32_t)off+count>13)return 2; /* Проверяет весь диапазон расширенного статуса до чтения четырёх дополнительных слов XY. */
w[0]=0x5747;w[1]=3;w[2]=s->running;w[3]=(uint16_t)s->rate;
w[4]=s->count;w[5]=s->received;w[6]=s->ready;w[7]=(uint16_t)s->capacity;w[8]=(uint16_t)(s->rate>>16);
w[9]=s->channels;w[10]=s->selected;w[11]=s->start_xy?2:1;w[12]=s->received_other; /* Расширение 0x1309…0x130C не меняет WG v3 и прежний ответ из восьми слов. */
for(i=0;i<count;++i)out[i]=w[off+i];
return 0;
}
unsigned set_wave_write(void *ctx,uint16_t off,uint16_t value) {
set_wave_state *s=(set_wave_state *)ctx;
if(!s||!s->samples||off>=9)return 2;
if(!s||!s->samples||off>=11)return 2; /* Запись разрешена до селектора 0x130A включительно; возможности и второй счётчик доступны только для чтения. */
return write_register(s,SET_WAVE_BASE+off,value);
}
unsigned set_wave_samples_read(void *ctx,uint16_t off,uint16_t count,uint16_t *out) {
set_wave_state *s=(set_wave_state *)ctx;unsigned i;
if(!s||!s->samples||!out||!count||(uint32_t)off+count>s->received)return 2;
for(i=0;i<count;++i)out[i]=s->samples[off+i];
for(i=0;i<count;++i)out[i]=sample_get(s,off+i) /* Возвращает выбранный канал из общей таблицы пар. */;
return 0;
}
unsigned set_wave_samples_write(void *ctx,uint16_t off,uint16_t value) {
@@ -77,21 +101,22 @@ unsigned set_wave_samples_write(void *ctx,uint16_t off,uint16_t value) {
return write_register(s,SET_WAVE_DATA+off,value);
}
size_t set_wave_rtu(set_wave_state *s,unsigned device,const uint8_t *req,size_t n,uint8_t *out,size_t cap) {
unsigned reg,value,error=0,i;uint16_t status[8];size_t length=0;
unsigned reg,value,error=0,i;uint16_t status[13]; /* Вмещает прежний статус и дополнительные регистры XY без изменения старого запроса. */size_t length=0;
if(!s||!s->samples||!req||!out||cap<21||n!=8||device<1||device>247
||req[0]!=device||set_crc16_modbus(req,n)) return 0;
reg=read16(req+2);value=read16(req+4);
if(!((reg>=SET_WAVE_BASE&&reg<SET_WAVE_BASE+9)||(reg>=SET_WAVE_DATA&&reg<SET_WAVE_DATA+SET_WAVE_MAX))) return 0;
if(!((reg>=SET_WAVE_BASE&&reg<SET_WAVE_BASE+13)||(reg>=SET_WAVE_DATA&&reg<SET_WAVE_DATA+SET_WAVE_MAX))) return 0; /* Общий RTU-кодек узнаёт расширенное окно XY и прежнюю таблицу без захвата чужих адресов. */
out[0]=(uint8_t)device;out[1]=req[1];
if(req[1]==6) {error=write_register(s,reg,value);memcpy(out,req,6);length=6;}
else if(req[1]==3) {
(void)set_wave_read(s,0,8,status);
if(reg==SET_WAVE_BASE&&value==8) {
out[2]=16;for(i=0;i<8;++i) write16(out+3+2*i,status[i]);length=19;
} else if(reg==SET_WAVE_BASE+8&&value==1) {
out[2]=2;write16(out+3,s->rate>>16);length=5;
} else if(reg>=SET_WAVE_DATA&&value==1&&reg-SET_WAVE_DATA<s->received) {
out[2]=2;write16(out+3,s->samples[reg-SET_WAVE_DATA]);length=5;
if(reg>=SET_WAVE_BASE&&reg<SET_WAVE_BASE+13&&value&&value<=13) { /* Отдельный RTU-кодек читает те же регистры, что общий сервер SET v2. */
error=set_wave_read(s,(uint16_t)(reg-SET_WAVE_BASE),(uint16_t)value,status); /* Проверяет весь диапазон до формирования ответа. */
if(!error) { /* Ошибочный диапазон возвращает исключение без чтения за пределами статуса. */
if(cap<5u+2u*value)return 0; /* Не пишет ответ расширенного статуса в прежний короткий буфер. */
out[2]=(uint8_t)(2u*value);for(i=0;i<value;++i)write16(out+3+2*i,status[i]);length=3u+2u*value; /* Отдаёт точное запрошенное число слов с прежним порядком байтов Modbus. */
} /* CRC добавляется общим seal после формирования тела. */
} else if(reg>=SET_WAVE_DATA&&value==1&&reg-SET_WAVE_DATA<s->received) { /* Чтение одного отсчёта сохраняет старый wire-контракт. */
out[2]=2;write16(out+3,sample_get(s,reg-SET_WAVE_DATA));length=5; /* XY возвращает выбранную координату, mono возвращает прежний uint16_t. */
} else error=2;
} else error=1;
if(error) {out[1]|=0x80;out[2]=(uint8_t)error;length=3;}
@@ -111,6 +136,9 @@ size_t set_wave_request(unsigned device,unsigned op,unsigned index,unsigned valu
case 7: if(index>=SET_WAVE_MAX)return 0;reg=SET_WAVE_DATA+index;fn=3;value=1;break;
case 8: reg+=8;fn=3;value=1;break;
case 9: reg+=8;if(value>(SET_WAVE_MAX_RATE>>16))return 0;break;
case 10: reg+=9;if(value<1||value>2)return 0;break; /* Выбирает один или два синхронных канала через существующий RTU/SET v2 транспорт. */
case 11: reg+=10;if(value>1)return 0;break; /* Выбирает X=0 либо Y=1 для прежних команд sample/block/readback. */
case 12: reg+=9;fn=3;value=4;break; /* Читает режим, селектор, возможности порта и количество отсчётов второй координаты. */
default:return 0;
}
out[0]=(uint8_t)device;out[1]=(uint8_t)fn;write16(out+2,reg);write16(out+4,value);return seal(out,6);
@@ -131,7 +159,7 @@ int set_wave_response(const uint8_t *req,const uint8_t *reply,size_t size,uint16
if(req[1]==6) {if(memcmp(req,reply,8)||cap<1)return -1;words[0]=read16(req+4);return 1;}
if(reply[2]!=expected-5||cap<(expected-5)/2)return -1;
for(i=0;i<(expected-5)/2;++i)words[i]=read16(reply+3+2*i);
if(read16(req+2)==SET_WAVE_BASE&&(words[0]!=0x5747||(words[1]<1||words[1]>3)))return -2;
if(read16(req+2)==SET_WAVE_BASE&&read16(req+4)>=2&&(words[0]!=0x5747||(words[1]<1||words[1]>3)))return -2; /* Версия проверяется только при наличии двух слов, исключая чтение за границей короткого ответа. */
return (int)((expected-5)/2);
}
@@ -168,13 +196,13 @@ size_t set_wave_block_rtu(set_wave_state *s,unsigned device,const uint8_t *req,
/* Preflight the whole block, including replayed prefix, before writing. */
for(i=0;i<count;++i) {
unsigned value=read16(req+7+2*i);
if(value>4095||(index+i<s->received&&s->samples[index+i]!=value)) {
if(value>4095||(index+i<s->received&&sample_get(s,index+i)!=value)) {
error=3;break;
}
}
if(!error) {
if(cap<8)return 0;
for(i=0;i<count;++i)s->samples[index+i]=read16(req+7+2*i);
for(i=0;i<count;++i)sample_set(s,index+i,read16(req+7+2*i)); /* После preflight записывает весь блок только выбранной координаты. */
if(index+count>s->received)s->received=(uint16_t)(index+count);
memcpy(out,req,6);length=6;
}
@@ -184,7 +212,7 @@ size_t set_wave_block_rtu(set_wave_state *s,unsigned device,const uint8_t *req,
else {
if(cap<5+2*(size_t)count)return 0;
out[2]=(uint8_t)(2*count);
for(i=0;i<count;++i)write16(out+3+2*i,s->samples[index+i]);
for(i=0;i<count;++i)write16(out+3+2*i,sample_get(s,index+i)); /* Чтение блоков использует тот же селектор, что и запись. */
length=3+2*(size_t)count;
}
} else error=1;
+1 -1
View File
@@ -4,7 +4,7 @@
{"name":"zoom_x_anchor","op":0,"input":[0,0,1,1,2,1,0,0,0.25,0.5],"output":[0.125,0,0.5,1]},
{"name":"zoom_y_anchor","op":0,"input":[0,0,1,1,1,4,0,0,0.5,0.75],"output":[0,0.5625,1,0.25]},
{"name":"pan_clamp","op":0,"input":[0.25,0.25,0.5,0.5,1,1,20,-20,0.5,0.5],"output":[0,0.5,0.5,0.5]},
{"name":"zoom_limit","op":0,"input":[0,0,1,1,1000,1000,0,0,0.5,0.5],"output":[0.49609375,0.49609375,0.0078125,0.0078125]},
{"name":"zoom_limit","op":0,"input":[0,0,1,1,1e15,1e15,0,0,0.5,0.5],"output":[0.4999999999995,0.4999999999995,1e-12,1e-12]},
{"name":"ignore_invalid_zoom","op":0,"input":[0.25,0.25,0.5,0.5,0,1,0,0,0.5,0.5],"output":[0.25,0.25,0.5,0.5]},
{"name":"horizontal_spread","op":1,"input":[100,2,8],"output":[1]},
{"name":"vertical_contract","op":1,"input":[2,-100,8],"output":[2]},
+3 -2
View File
@@ -26,6 +26,7 @@ SOURCES = [
ROOT / "src" / name for name in (
"deif_xdi.c", # Общий DEIF входит в host-библиотеку.
"set_invertor_gas.c", # Собирает общий адаптер двух GAS для STM Invertor при прямом запуске host-builder.
"set_deif_gauge.c", "set_igbt_realtime.c",
"set_regmap.c", "set_emu_ump.c", "set_emu_tms.c", "set_emu_igbt.c", "set_emu_server.c", "set_boot.c",
"firmware_image.c", "altera_logic.c",
"altera_stream.c",
@@ -65,7 +66,7 @@ def _build_msvc(output: Path, build_dir: Path) -> None:
import_library = build_dir / "setprotocol.lib"
jni_flags = " ".join(f'/I"{path}"' for path in JNI_INCLUDES)
command = (
f'call "{vcvars}" && cl /nologo /W4 /std:c11 '
f'call "{vcvars}" && cl /nologo /W4 /O2 /std:c11 ' # Оптимизируем исходный C-решатель, чтобы сравнение с оптимизированным Eigen не измеряло различие режимов компиляции.
f'/DPCAN_ABI_BUILD_DLL /I"{INCLUDE}" {jni_flags} /LD {quoted_sources} '
f'/Fe"{output}" /link /IMPLIB:"{import_library}"'
)
@@ -78,7 +79,7 @@ def _build_cc(output: Path, build_dir: Path) -> None:
compiler = shutil.which("cc") or shutil.which("clang") or shutil.which("gcc")
if compiler is None:
raise SystemExit("C compiler was not found")
command = [compiler, "-std=c99", "-Wall", "-Wextra", "-Wpedantic",
command = [compiler, "-O2", "-std=c99", "-Wall", "-Wextra", "-Wpedantic", # Одинаковая оптимизация C99 и C++ делает время настольных вариантов сопоставимым.
"-fPIC", "-shared", f"-I{INCLUDE}",
*(f"-I{path}" for path in JNI_INCLUDES), *map(str, SOURCES),
"-o", str(output), "-lm"]
+50
View File
@@ -0,0 +1,50 @@
# Альтернативный МНК Eigen QR
Необязательный настольный модуль выполняет полиномиальный МНК через
`Eigen::ColPivHouseholderQR`. Исходный двухпроходный решатель остаётся вариантом
по умолчанию. Прошивки и основная SETProtocol DLL продолжают собираться как C99.
Путь вызова: GUI → Python/ctypes → `setsignal_eigen` → общая подготовка
`set_signal.c` → callback Eigen → общие вычисление кривой и RMSE.
Сортировка, усреднение повторов X, нормировка, сетка, рабочий буфер и аннотации
формул совпадают у обоих вариантов. Порог ранга Eigen относительный: `1e-12`;
он может иначе классифицировать пограничные данные, чем исходная ортогонализация.
Модуль не зависит от Qt и не участвует в обмене с устройствами.
| Файл | Назначение и зависимости |
| --- | --- |
| `signal_eigen.cpp` | Решатель QR и экспорт `set_signal_reconstruct_eigen`; C++14, Eigen, `set_signal.h` |
| `build.py` | Сборка DLL/SO, проверка и распаковка локального архива; Python 3.8+ и C/C++ компиляторы |
| `vendor/eigen-3.4.0.tar.gz` | Оригинальный архив зависимости, без изменений |
| `../../python/set_devices/signal_eigen.py` | Ленивый ctypes-загрузчик отдельного компонента |
Зависимость: [Eigen 3.4.0, официальный архив](https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.gz).
SHA-256: `8586084f71f9bde545ee7fa6d00288b264a2b7ac3607b974e54d13e7162c1c72`.
Заголовки компилируются с `EIGEN_MPL2_ONLY`; `Eigen-COPYING.MPL2` копируется
рядом с библиотекой и включается в пакет SETGUI. Сборка не требует интернета.
Из каталога SETGUI:
```powershell
.venv/Scripts/python.exe third_party/templates/cpp/signal-eigen/build.py --output src/gui_desktop/native/setsignal_eigen.dll # Собираем 64-битный настольный компонент рядом с основной DLL.
```
Для 32-битного приложения добавьте `--arch x86`. Компилятор C обрабатывает
общий алгоритм отдельно от C++14-решателя. Windows-модуль использует статический
CRT и PE-подсистему 6.01; работоспособность на самой Windows 7 требует проверки
на соответствующей машине. Для Linux/macOS укажите имя `libsetsignal_eigen.so`
или `libsetsignal_eigen.dylib`. `scripts/build_exe.py` собирает и упаковывает
компонент автоматически, включая вариант Windows 7 x86.
Контракт `set_signal_fit_fn` описан в `c/set-protocol/include/set_signal.h`:
callback получает уникальные нормированные X/Y, количество точек и степень 1…5,
возвращает коэффициенты по возрастанию степеней аргумента `2*x-1`.
Владение выходами остаётся у вызывающего; Eigen выделяет временные матрицы
только в настольном порте. Коды ошибок: 0 — успех, 1 — параметры, 2 — нечисловой
результат, 3 — недостаточный ранг, 5 — память/ошибка C++-решателя.
Исключения не пересекают C ABI. Общих изменяемых рабочих буферов нет.
Загрузчик ищет компонент рядом с выбранной SETProtocol DLL. Переменная
`SETSIGNAL_EIGEN_LIBRARY` позволяет явно задать путь для тестов. Отсутствующая
библиотека вызывает понятную ошибку после выбора Eigen; скрытой замены
на исходный решатель нет. Используется в обработке графиков и генераторе SETGUI.
File diff suppressed because one or more lines are too long
+40
View File
@@ -0,0 +1,40 @@
#define EIGEN_MPL2_ONLY // Ограничиваем подключаемые части Eigen лицензией MPL-2.0.
#include <Eigen/QR> // Подключаем QR с выбором ведущего столбца для полиномиального МНК.
#include <cmath> // Проверяем конечность вычисленных коэффициентов перед передачей в C.
#include <new> // Перехватываем ошибку выделения памяти внутри границы C ABI.
#include "set_signal.h" // Используем общую подготовку точек, вычисление кривой и стабильный C ABI.
static int eigen_fit(const double *x,const double *y,size_t count,unsigned degree,double *coefficients) { // Вычисляем только коэффициенты по уже нормированным уникальным точкам.
try { // Исключения C++ не должны пересекать границу ctypes или C-ядра.
Eigen::MatrixXd design(count,degree+1); // Матрица имеет одну строку на точку и до шести степеней, включая свободный член.
for(size_t i=0;i<count;++i) { // Заполняем нормированную матрицу Вандермонда в том же базисе, что исходный МНК.
double power=1.0; // Нулевая степень задаёт столбец свободного члена без вызова pow.
for(unsigned j=0;j<=degree;++j) { // Последовательно строим степени от нулевой до выбранной пользователем.
design(i,j)=power; // Сохраняем коэффициент при соответствующей степени в строке измерения.
power*=2.0*x[i]-1.0; // Переводим нормированное время из [0,1] в [-1,1], сохраняя обусловленность исходного алгоритма.
} // Заканчиваем заполнение строки матрицы по одной временной точке.
} // Все точки участвуют в МНК с одинаковым весом после общего усреднения повторов X.
Eigen::ColPivHouseholderQR<Eigen::MatrixXd> qr(design); // Используем отражения Хаусхолдера с перестановкой столбцов вместо собственной ортогонализации.
qr.setThreshold(1e-12); // Относительный порог ранга соответствует порядку допуска исходного решателя; он не является абсолютной погрешностью сигнала.
if(qr.rank()!=static_cast<Eigen::Index>(degree+1)) return 3; // Отклоняем вырожденный базис, не выдавая неоднозначные коэффициенты за корректную кривую.
Eigen::Map<const Eigen::VectorXd> values(y,count); // Читаем нормированные амплитуды из памяти C без дополнительного копирования входного вектора.
Eigen::VectorXd solution=qr.solve(values); // Находим минимум суммы квадратов без формирования нормальных уравнений AᵀA.
for(unsigned j=0;j<=degree;++j) { // Возвращаем степени в исходном порядке: Eigen отменяет перестановку столбцов при solve.
if(!std::isfinite(solution[j])) return 2; // Не разрешаем NaN или бесконечности попасть в оценку кривой и RMSE.
coefficients[j]=solution[j]; // Передаём коэффициент в буфер общего C-алгоритма вычисления полинома.
} // Весь выходной полином проверен до передачи управления C-ядру.
return 0; // Сообщаем общий для двух реализаций код успешного расчёта.
} catch(const std::bad_alloc &) { // Преобразуем нехватку памяти необязательного настольного решателя в детерминированный код.
return 5; // Код 5 обозначает невозможность выполнить расчёт, включая выделение матриц.
} catch(...) { // Защищаем потребителей C ABI от остальных исключений библиотеки C++.
return 5; // Возвращаем явную ошибку без подмены выбранного пользователем решателя.
} // Завершаем защиту границы между C++ и C.
} // Решатель не хранит глобальных данных и допускает независимые параллельные вызовы.
extern "C" PCAN_ABI_API int set_signal_reconstruct_eigen( // Отдельный экспорт сохраняет возможность загрузить модуль без изменения основной DLL.
const double *x,const double *y,size_t count,int method,unsigned degree, // Формат точек и предел степени совпадают с исходным API.
size_t output_count,int endpoint,double *out_x,double *out_y, // Временная сетка и включение конечной точки рассчитываются общим C-ядром.
double *meta,double *work,size_t work_count) { // Метаданные и рабочая память сохраняют формат аннотаций формулы GUI.
if(method!=SET_SIGNAL_POLYNOMIAL) return 1; // Eigen доступен только для МНК; остальные методы не маскируются под альтернативный решатель.
return set_signal_reconstruct_with_fit(x,y,count,method,degree,output_count,endpoint,out_x,out_y,meta,work,work_count,eigen_fit); // Повторно используем C99-подготовку, обработку повторов, вычисление кривой и RMSE.
} // Завершаем экспорт с соглашением вызова C для x86 и x64.
Binary file not shown.
+1
View File
@@ -11,6 +11,7 @@ Device Name — отображаемое имя адреса, а не допол
| `0x2` | `0x0…0xF` | PERIPH 16–31 | Вторая группа; номер PERIPH = 16 + DeviceID |
| `0x1` | `0x5` | **УМП1** | PERIPH 5, собственный адрес обычного обмена ProtoCAN и загрузчика |
| `0x1` | `0x6` | УМП2 | PERIPH 6, GAS 0x3 — регистры; GAS SLOT 0x4 — слоты 0x0600…0x0642 |
| `0x1` | `0x8` | **Задатчик** | PERIPH 8; для ПМ35 с M95M02 сохраняется ProductType `0x2833` |
| `0x6` | `0xE` | Altera Logic | Отдельный существующий потоковый профиль FE00/FF00 |
| `0x7` | `0xD` | configurator | Существующий конфигуратор |
| `0x7` | `0xE` | **STM Invertor** | Самостоятельный ПЧ STM32G474VET6; отдельный GAS-профиль на 250 слотов 0x4, 750 значений; Word0 — маска |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+75
View File
@@ -147,3 +147,78 @@ CAN ID = 0x13590702
Этот пример соответствует `ENTER_BOOT`, `SessionID=7`. Машинные варианты
находятся в [examples/test-vectors.json](examples/test-vectors.json).
## GAS: сквозная карта УКСС и профиль 167/v1
УКСС назначены десятичные ID **0…11999**, блоки по 1000 слов:
`GAS_ID = номер_узла × 1000 + смещение`. Блок 0 — общий резерв;
узлы 1…11 и все границы описаны в
[GAS_ADDRESS_ALLOCATION_167.md](GAS_ADDRESS_ALLOCATION_167.md).
Диапазон 12000…65535 свободен для последующего распределения.
УМП 1 (узел 5) назначены ID 5000…5127, УМП 2 (узел 6) —
6000…6127. Смещения 128…999 зарезервированы. Полный словарь 128 слов,
ограничения LOAD, биты Commands/LOAD_STATUS и соответствие Modbus приведены в
[GAS_MODBUS_MAP_167.md](GAS_MODBUS_MAP_167.md).
Новые ID пока назначены только в документации. Действующий профиль 167/v1
использует Modbus 0x0000…0x007F и окно GAS 0x5000…0x507F;
например, LOAD_CAN_TIMEOUT: Modbus 0x0052, GAS 0x5052,
новые ID 5082/6082. Десятичный ID 5000 не равен hex-адресу 0x5000.
Служебный CAN УМП с маркером 0x1F00 — отдельный транспорт;
его пакет нельзя интерпретировать как обычный payload ProtoCAN GAS.
Словарь и lookup находятся в `python/protocan/gas_167.py` и
`gas_167_registers.json` проекта templates. Парсер обычных кадров GAS
показывает назначенные имена, узел, старые адреса и статус для запросов
и каждого слова ответа; неизвестные и резервные адреса сохраняются raw.
Общая сводка `Decoded.summary` содержит имя каждого назначенного параметра,
УМП 1/2, десятичный ID, hex-адрес и значение в dec/hex. Поэтому имена видны
непосредственно в колонке «Расшифровка» всех клиентов общего декодера:
онлайн-монитор, трассировка, фильтр кадров, ручной разбор и DevBoard_V1.
Полный текст длинной строки доступен во всплывающей подсказке.
Тип сообщения отображается как `GAS (General Address Space)`.
Обычные демо-потоки шины и DevBoard также включают назначенные адреса УМП.
Низкие Modbus-адреса и старое окно не определяют профиль прибора автоматически.
DATA обычного ProtoCAN GAS — до четырёх u16 little-endian; Modbus RTU
передаёт каждое слово старшим байтом вперёд. GUI использует этот же словарь
через совместимый импорт, без собственной реализации карты.
### Демо УМП в SETGUI
Общий список «Протокол» позволяет выбрать `ProtoCAN / GAS`,
`УМП / Balsam 167 (legacy CAN)`, карту `GAS 167/v1` отдельно для УМП 1/2
или `УМП / ProtoCAN GAS — новые ID 5000/6000`.
Выбор перерисовывает уже полученные кадры. Для старой карты номер УМП
выбирается оператором и обозначается «выбран вручную»; низкие адреса
0…127 и окно 0x5000…0x507F сами по себе номер узла не кодируют.
Этот режим накладывает словарь на слова ProtoCAN GAS/Modbus Holding;
он не является декодером служебных CAN-пакетов с маркером 0x1F00.
Профиль Balsam принимает только собственный формат и показывает
несоответствие для чужих кадров. В сводке Commands показаны активные
биты с названиями команд; это не подтверждение их выполнения.
В режиме «Авто» legacy-ID Balsam проверяются первыми. Для корректного
ProtoCAN GAS с целыми словами все адреса кадра сверяются с назначенными
диапазонами УМП. Если все они принадлежат одному узлу, поля Device Name
и Msg Type показывают УМП 1/2; основание выбора записывается в подробности
«Автоопределение». Это распознавание назначенной карты, а не подтверждение
её поддержки прошивкой. Для старых/неизвестных адресов, смешанных диапазонов
и некорректного payload автоматический выбор УМП не выполняется;
сводка сообщает «профиль/узел не определены». Номер Device в ProtoCAN сам
по себе не считается номером УМП. «Демо УМП» запускается с режимом «Авто».
Открыть `CAN → Шина CAN / мост → Настройки → Демо УМП`.
Генератор `python/set_devices/bus_demo.py:UmpDemoSource` моделирует два
узла с демонстрационными адресами `Device Type=3`, `Device=5/6`.
Он передаёт PULSE раз в секунду и четыре GAS-слова каждого узла каждые
250 мс: ID 5000…5127 и 6000…6127, полный обход за 8 секунд.
Это имитация назначенной карты, а не эмуляция текущего транспорта 167/v1.
Кадры проходят обычное кадрирование AA 55, CRC и общий парсер ProtoCAN.
Их можно смотреть в онлайн-мониторе, трассировке и адресном пространстве;
в буфере сессии и его CSV источник обозначен `DEMO УМП`.
Значения синтетические, в формате raw u16. LOAD_STATUS показывает RUN
(бит 3), LOAD_ATTEMPTS — 0, LOAD_CAN_TIMEOUT — 1000 мс.
Кнопка «Стоп демо УМП» останавливает поток. Для запуска нужно отключить
COM-порт и CAN-снифер; генератор работает локально без передачи в оборудование.
+2 -2
View File
@@ -642,11 +642,11 @@ CRC32 обнаруживает случайное повреждение, но
<td class="x14">&nbsp;</td>
<td colspan="8" class="x96">64 битовых адресса</td>
</tr>
<tr style="height:17px">
<tr style="height:72px">
<td class="x26">&nbsp;</td>
<td class="x27">&nbsp;</td>
<td colspan="3" class="x131">0x3</td>
<td colspan="4" class="x136">0x0 - 0xF устройств</td>
<td colspan="4" class="x136">0x0 - 0xF устройств; прежнее общее назначение F28335 3/7 отменено</td>
<td colspan="12" class="x138">0x3. Установка времени</td>
<td colspan="4" class="x140">&nbsp;</td>
<td class="x14">&nbsp;</td>
+210
View File
@@ -0,0 +1,210 @@
# Сравнение сигналов с CSV-эталоном
Общее ядро `set_devices.signal_reference` и Qt-порт
`set_devices.qt_ports.reference_comparison` сравнивают измеренную запись с
эталоном. Они не импортируют приложение, не выбирают его каталоги и не
требуют конкретного движка отрисовки. Ядро использует только стандартную
библиотеку Python; Qt-порт поддерживает PySide6 и PySide2.
## Численный API
`Comparison.analysis` содержит локальный параметрический отчёт:
`summary`, `details`, `attention`, `findings`, `features`, `model_report`.
Он рассчитывается `set_devices.signal_diagnostics` по тому же интервалу,
временному сдвигу и допускам. Исходные метрики используют все общие отсчёты;
объём выборки подбора моделей явно указан в `model_report`.
Добавлено необязательное поле с пустым словарём по умолчанию: прежний
конструктор `Comparison` с пятью аргументами сохраняет совместимость.
Для одинаковых известных обозначений `Uce`, `Uge`, `Ic` анализ различает
повторные полные переходы, усиленный звон Uce после выключения, отсутствие
переключения, растянутые фронты и временной сдвиг. Общие причины включают
смещение, шум и неопределённое изменение формы. Разные известные физические
каналы, недостаточное число точек и неполное покрытие отражаются в выводе.
На неизвестных каналах аппаратные гипотезы о снаббере и транзисторе не строятся.
`set_devices.signal_fault_models` подбирает смещение, масштаб, дрейф,
задержку, растяжение фронтов, постоянный уровень, кусочные прерывания и
затухающий отклик RLC. `set_devices.signal_joint_models` добавляет локальную
ВАХ `Uce=U0+Rdiff*Ic`, остаточный ток выключенного состояния и совпадение
провалов управления/проводимости по трём каналам.
`compare(..., circuit=..., channels=...)` принимает необязательные известные
`inductance_h`, `capacitance_f`, `resistance_ohm` в СИ и словарь связанных
каналов `dict(measured=..., reference=...)`. GUI формирует эти снимки сам.
Однозначность физических имён и единиц проверяется до совместного анализа.
Глобальные модели сравниваются по BIC общей обучающей выборки и RMSE на
каждой третьей отложенной точке. JSON сохраняет всех конкурентов, число
параметров, формулы, критерии и ограничения. BIC не интерпретируется как
вероятность аппаратного отказа. Без известного элемента R/L/C не выдаются
три произвольных номинала: доступны только LC и R/L последовательного
эквивалента полюсов.
`ComparisonAnalysis` в `qt_ports.reference_results` показывает одну строку
и кнопку отчёта с таблицей моделей и подробным текстом по образцу Android GUI. Компонент
используется и над исходным графиком, и в полной форме сравнения; изменение
входов, чтение другого эталона и ошибки очищают прежний анализ. JSON-экспорт
обоих видов включает `analysis` рядом с `metrics`. Ядро не использует сеть
или обученную модель; пороги признаков и ограничения указаны в подробностях.
`Trace(name, times, values)` содержит имя, строго возрастающие временные
метки в миллисекундах и амплитуды в единицах канала. `trace_from_pairs`
создаёт такой снимок из пар `(time_ms, value)`; необязательный `transform`
применяет множитель или иное преобразование амплитуды.
`read_reference(path, cancel=...)` потоково читает UTF-8 CSV с необязательным
BOM. Первый столбец — `time_ms` либо `timestamp` с ISO-временем и явным
часовым поясом. Остальные столбцы имеют непустые уникальные имена каналов.
Поддерживаются запятая, точка с запятой и табуляция. Пустые ячейки и NaN
сохраняются как пропуски; повторное время, бесконечности и повреждённые
строки отклоняются.
```python
from set_devices.signal_reference import compare, read_reference, trace_from_pairs # Ядро можно использовать без Qt и графика.
measured = trace_from_pairs('U (V)', [(0., 2.), (1., 4.), (2., 6.)]) # Время задано в миллисекундах, амплитуда — в вольтах.
reference = read_reference('reference.csv')['U (V)'] # Столбец выбирается по полному имени канала.
result = compare(measured, reference, absolute_tolerance=.1, relative_tolerance_pct=2.) # Допуск равен 0,1 В плюс 2% модуля эталона.
print(result.metrics['rmse']) # Показатель рассчитан по всем корректным точкам измерения в общем интервале.
```
`compare` возвращает `Comparison`: нормализованные кривые `measured` и
`reference`, остаток `difference`, эталон на сетке остатка `expected` и
словарь `metrics`. Статистика включает покрытие, пропуски, среднее смещение,
MAE, RMSE, СКО без смещения, максимальную ошибку и выходы за допуск.
Эталон линейно интерполируется на исходную сетку измерения без
экстраполяции и соединения через NaN. Остаток равен измерению минус эталон.
Допуск равен `absolute_tolerance + relative_tolerance_pct / 100 * abs(reference)`;
равенство границе допуска не считается превышением. Экранная децимация
не участвует в расчёте.
По умолчанию начала записей совмещаются. `align_start=False` сохраняет
различие абсолютных начал; положительный `shift_ms` сдвигает эталон вправо.
`interval=(start_ms, stop_ms)` задаётся от начала измерения, обе границы
включены. Callback `cancel()` прерывает подготовку, чтение и сравнение через
`InterruptedError`.
## Наложение на существующий график
Для добавления эталона в уже работающий график используйте
`set_devices.qt_ports.reference_overlay.ReferenceOverlayPanel`. Это
компактные параметры без canvas. Оригинальный график
остаётся у потребителя и продолжает показывать измерение при загрузке и
изменении эталона.
Конструктор принимает `sources`, `parent`, `source_path` и необязательный
`path_provider` с тем же смыслом, что в таблице ниже. `load_reference(path)`
автоматически читает CSV, совмещает выбранные каналы и рассчитывает показатели
в рабочем потоке. Отдельной кнопки запуска расчёта нет. Изменение канала,
сдвига, совмещения начал или допусков обновляет результат автоматически.
Сигнал `changed` сообщает потребителю, что `overlay()` вернёт актуальные
спецификации и точки одной дополнительной кривой. Потребитель **добавляет**
их к оригинальным рядам и общей легенде своего canvas. `RESULT_KEY`
отличает эталон от измерений; оранжевая кривая имеет подпись
`Эталон: <канал>`. Временные метки эталона уже находятся в исходной шкале
измерения: начало оригинала не обнуляется. Потребитель применяет своё
перебазирование короткой развёртки к обеим кривым одинаково.
`set_sources(sources)` обновляет кадр прибора или преобразование амплитуд,
сохраняя загруженный эталон. Сигнал `remove_requested` предлагает владельцу
удалить дополнительный слой и параметры. `report()` и раскрываемая строка
«Допуски и отчёт» предоставляют актуальную статистику и экспорт JSON.
Если общей части времени нет, кривая остаётся доступна, а показатели
заменяются объяснением отсутствия перекрытия. `close()` останавливает worker,
не закрывая график потребителя.
Истории, переданные в одном снимке `sources`, должны быть неизменяемыми;
для нового кадра передайте новую историю через `set_sources`. Порт
поддерживает PySide6 и PySide2 без импорта приложения.
### Подробные показатели рядом с исходным графиком
`ReferenceOverlayPanel.results` — виджет `ComparisonResults` с полной
таблицей всех 14 показателей: покрытие, пропуски, смещение, MAE, RMSE,
СКО, максимум и его время, нормированная ошибка и выходы за допуск.
Потребитель размещает его в существующей боковой области рядом с canvas;
в SETGUI он открывается автоматически во вкладке «Отклонения и расчёты».
Сигнал `results_requested` кнопки «Расчёты» позволяет вернуться к этой
области после выбора обычной легенды.
Флажок `results.delta_curve` добавляет в `overlay()` кривую с ключом
`DIFFERENCE_KEY`. Её временные метки находятся в той же исходной шкале,
что эталон и оригинал. Исходные кривые сохраняются, отдельный canvas не
создаётся. Кнопка JSON рядом с таблицей экспортирует тот же актуальный
результат. Изменение параметров и ошибки очищают устаревшие показатели.
`comparison_rows(metrics)` из `qt_ports.reference_results` задаёт единые
подписи, порядок и единицы как для этой таблицы, так и для прежнего диалога.
## Совместимые Qt-панель и отдельное окно
`ReferenceComparisonPanel` — обычный `QWidget` для существующей страницы
графика. Кнопка «К записи» испускает `back_requested`; потребитель сам
скрывает панель и возвращает исходный вид. При повторном показе той же
панели сохраняются эталон, параметры и рассчитанные показатели.
`ReferenceComparisonDialog` — отдельный `QDialog` с тем же расчётом и
настройками. Оба конструктора принимают:
| Параметр | Контракт |
| --- | --- |
| `sources` | Последовательность пар `(source, points)`: `source.name` — имя, `source.display_value(value)` — преобразование амплитуды, `points` — полная неизменяемая история `(time_ms, value)` |
| `parent` | Родительский QWidget; для встроенной панели передайте страницу графика |
| `source_path` | Происхождение измерения для JSON-отчёта, по умолчанию пустая строка |
| `plot_factory` | Обязательная фабрика без аргументов, возвращающая настроенный QWidget графика |
| `series_factory` | Обязательная фабрика `factory(key, name, color=..., timestamps=..., values=...)`, создающая кривую потребителя |
| `path_provider` | Необязательный callback `filename → начальный путь`; пустое имя используется для открытия эталона, `reference_comparison.json` — для отчёта |
```python
from set_devices.qt_ports.reference_comparison import ReferenceComparisonPanel # Qt загружается только при импорте конкретного порта.
panel = ReferenceComparisonPanel( # Один компонент обслуживает выбор каналов, параметры, фоновый расчёт и экспорт.
sources=recording_channels, # Потребитель фиксирует полные истории и преобразование амплитуд до создания панели.
parent=chart_page, # Сравнение размещается в существующей рабочей области.
source_path=recording_filename, # Имя исходной записи попадёт в отчёт.
plot_factory=create_comparison_canvas, # Адаптер создаёт график с единой шкалой Y для обеих кривых.
series_factory=create_series, # Контейнеры кривых принадлежат используемому движку отрисовки.
path_provider=recording_path, # Папки и правила хранения остаются политикой потребителя.
) # Все функции и объекты адаптера задаются приложением до этого вызова.
chart_page.layout().addWidget(panel, 1) # Панель занимает свободное место страницы.
panel.back_requested.connect(show_recording) # Обработчик скрывает сравнение и возвращает исходный график.
```
Canvas должен предоставлять изменяемый список `_series`, методы
`reset_interaction()`, `_sync_bounds()`, стандартные `update()` и `close()`,
флажки `axis_lock_x` и `axis_lock_y` с `setChecked()`, поле `axis_limits`,
`visible_bounds.left/right` в миллисекундах и `viewport` типа
`protocan.plot.Viewport`. `_sync_bounds()` задаёт конечный ненулевой диапазон
X даже для пустого графика. Обе наложенные кривые используют **одну шкалу Y**;
автоматическое разделение каналов на независимые оси отключает адаптер.
Дополнительные кнопки canvas, подписи и правила снимков также настраиваются
в `plot_factory`.
Панель передаёт фабрике кривых полные массивы: их экранное сокращение остаётся
ответственностью canvas. Временные поля интерфейса показывают микросекунды,
ядро и JSON-отчёт используют миллисекунды. Явно различающиеся единицы в
скобках имён каналов блокируют сравнение; автоматической конвертации нет.
Истории и настройки источников нельзя менять, пока панель использует их
снимок. При смене записи или преобразования канала закройте старую панель
и создайте новую. `close()` встроенной панели отменяет и дожидается worker;
уничтожение родителя также останавливает поток до удаления дочерних объектов.
Отдельный диалог откладывает закрытие до завершения кооперативной отмены.
`load_reference(path)` позволяет открыть эталон программно, `report()`
возвращает актуальный словарь отчёта либо `None`. Изменение входных
параметров очищает старые кривые и показатели до следующего расчёта.
## Проверки
`python/tests/test_reference_comparison_port.py` проверяет внедрение
стороннего canvas, преобразование амплитуд, отклонение и экспорт с внешним
provider путей, а также API отдельного диалога. Его можно запустить из
каталога `templates/python` командой
`python -m unittest discover -s tests -p test_reference_comparison_port.py`.
`python/tests/test_shared_library_boundary.py` проверяет независимость
вычислительных модулей от GUI и Qt. Интеграционные проверки потребителя
дополнительно покрывают полные истории, общий масштаб Y, отмену, смену
записи, возврат к графику и полноэкранный режим.
+7
View File
@@ -15,6 +15,10 @@
| `set_devices.qt_ports` | UART/mock, SLCAN, Candle/WinUSB, STM/TMS boot, STM settings, UMP CAN |
| `set_devices.plot_processing` | Общий контракт графиков: аппроксимация, интерполяция, восстановление, единицы и CSV через C99 |
| `set_devices.qt_ports.plot_processing` | Переиспользуемая панель всех методов, фоновый расчёт и отдельный слой кривой |
| `set_devices.signal_reference` | Сравнение полной записи с CSV-эталоном: совмещение времени, допуски, остаток и показатели ошибки без Qt |
| `set_devices.qt_ports.reference_comparison` | Общие встроенная панель и диалог сравнения, фоновое чтение/расчёт, внедряемые график и папки, JSON-отчёт |
| `set_devices.qt_ports.reference_overlay` | Компактные параметры и автоматически подготовленный эталон для наложения на существующий canvas без создания второго графика |
| `set_devices.qt_ports.reference_results` | Полная таблица отклонений, единое форматирование 14 показателей, флажок кривой Δ и экспорт |
| `protocan`, `setprotocol` | Существующие обёртки общего C99-ядра и SET v2 |
Импорт `logic_analyzer` / `set_devices` не загружает Qt и не зависит от GUI.
@@ -24,6 +28,9 @@ Saleae SDK подключается при обращении к устройс
в приложении. Общая панель обработки графиков размещена в `qt_ports.plot_processing`:
[контракт, подключение нового графика и примеры](../c/set-protocol/docs/PLOT_PROCESSING.md).
Сравнение с эталоном подключается через фабрики canvas и кривой без
зависимости от приложения: [API ядра, контракт Qt-порта и пример](../docs/reference-comparison.md).
Конструкторы CAN-адаптера, CAN DevBoard и RS485 DevBoard используют общий
компонент `set_devices.qt_ports.can_frame_builder.CanFrameFields`. Он задаёт
одинаковые поля и исходный режим «Обычный CAN — ID / DLC / Data», сохраняет
+2 -13
View File
@@ -16,20 +16,9 @@ BOOT = dict(enumerate(('IDENTIFY ENTER_BOOT BEGIN_IMAGE BEGIN_COMPAT ERASE VERIF
def legacy(ident, data, profile='protocan'):
if not 0 <= ident <= 0x1fffffff or len(data) > 8:
raise ValueError('Invalid classic CAN frame')
relative = ident - 0xba0000
if profile == 'balsam':
if not (0 <= relative < 13 or 16 <= relative < 29):
return None
if len(data) != 8:
raise ValueError('Balsam requires DLC=8')
header = number(data[:2], 'big')
address, mask = header & 0x1fff, header >> 13
registers = [(address + i, number(data[2+i*2:4+i*2], 'big'))
for i in range(3) if mask & (4 >> i)]
return dict(protocol='Balsam', registers=registers, summary=
'Balsam node=%d %s mask=%X %s' % ((relative & 15) + 1,
'data' if relative >= 16 else 'command', mask,
' '.join('R%04X=%04X' % pair for pair in registers)))
from .template_protocols import decode_can # Единая реализация templates знает порядок value2/value3/header/value1 и специальные ID пульта.
return decode_can(ident, data, 'balsam') # Старый публичный вызов получает исправленную расшифровку без второго варианта wire-формата.
msg, body = (ident >> 16) & 15, ident & 0xffff
result = dict(protocol='ProtoCAN', priority=(ident >> 28) & 1,
route=(ident >> 27) & 1, device_type=(ident >> 24) & 7,
@@ -0,0 +1,63 @@
from pathlib import Path # Путь к поставляемым библиотекам вычисляется относительно ядра декодера, независимо от рабочего каталога DSView.
import sys # Встроенному Python требуется путь к переносимым пакетам, которые поставляются вместе с декодерами.
from types import SimpleNamespace # Общим CAN-парсерам нужен только набор свойств кадра, без подключения транспорта или Qt.
_vendor = Path(__file__).with_name('templates') # Установщик помещает исходные пакеты templates в закрытый подкаталог общего ядра.
if _vendor.is_dir() and str(_vendor) not in sys.path: # При запуске из исходников используются штатные пакеты; установленный DSView получает собственные копии.
sys.path.insert(0, str(_vendor)) # Локальные версии пакетов имеют приоритет и не требуют установленного SETGUI или внешнего Python.
from protocan import balsam, legacycan, protocan as pc # Форматы ID, порядок слов и каталоги проектов берутся из действующих библиотек templates.
from set_devices import deif_xdi, ump_can # Профиль DEIF и словарь УМП используются теми же функциями, что в CAN-мониторе.
CAN_PROFILES = ('protocan', 'auto', 'devboard', 'balsam', 'ump-protocan', 'deif', 'legacy', 'set-v2') # Старые ключи сохранены для ранее настроенных записей DSView.
LEGACY_PROJECTS = tuple(project.name for project in legacycan.PROJECTS) # Список выбора обновляется вместе с единым каталогом исторических проектов.
def _decoded(result): # Преобразует нейтральную модель CAN-монитора в словарь событий анализатора.
return dict(protocol=result.protocol, summary=result.summary, fields=list(result.fields), registers=result.registers, warnings=list(result.warnings), device=result.device_label) # Копии списков исключают изменение результата другим слоем декодирования.
def _balsam(ident, payload): # Разбирает фактический порядок слов Balsam 167 вместо упрощённого заголовка в начале данных.
result = balsam.decode(ident, payload, native=pc._SHARED_PROTOCOL) if pc._SHARED_PROTOCOL is not None else balsam._decode_fallback(ident, payload) # Без DLL используется существующий переносимый разбор; повторный поиск DLL на каждом кадре исключён.
if result is None: # Чужой идентификатор не должен получать подпись Balsam.
return None # Отсутствие подходящего кадра не создаёт ложную ошибку на общей шине.
registers = [(item.address, item.value) for item in result.registers] # Общий парсер уже применил маску присутствия и восстановил порядок трёх слов.
fields = [('Узел', result.device_name), ('Маршрут', result.route), ('Начальный адрес', '0x%04X' % result.start_address), ('Маска слов', format(result.present_mask, '03b'))] # Адрес имеет ширину 13 бит, а маска содержит три признака из wire-формата Balsam.
return dict(protocol='Balsam', summary=result.summary, registers=registers, fields=fields, warnings=[]) # Сохраняет совместимое имя протокола и полную сводку с командами общего парсера.
def decode_can(ident, payload, profile='protocan', *, extended=True, ump_map='gas167', ump_node=1, project=LEGACY_PROJECTS[0]): # Явные настройки предотвращают угадывание несовместимых карт регистров по содержимому данных.
payload = bytes(payload) # Парсеры получают неизменяемые байты вне зависимости от контейнера, переданного sigrok.
if not 0 <= ident <= (0x1fffffff if extended else 0x7ff) or len(payload) > 8: # Classic CAN допускает 29-битный EXT либо 11-битный STD ID и не более восьми байт.
raise ValueError('Invalid classic CAN frame') # Повреждённый ID или DLC отклоняется до интерпретации прикладных полей.
if profile not in CAN_PROFILES or profile == 'set-v2': # Сегментированный SET v2 обрабатывается отдельной машиной состояний с тайм-аутом.
raise ValueError('Unsupported CAN profile: ' + profile) # Неизвестный профиль не подменяется ProtoCAN молча.
if not extended and profile != 'deif': # Все поставляемые профили SET используют EXT; DEIF работает со стандартными CANopen ID.
return None # Стандартный кадр не интерпретируется как адресное поле ProtoCAN.
frame = SimpleNamespace(can_id=ident, data=payload, ide=extended, rtr=False, is_error=False) # Минимальный кадр соответствует контракту общих функций CAN-монитора.
if profile == 'deif': # CANopen/DEIF использует собственную адресацию и проверку селектора.
return _decoded(deif_xdi.decode(frame)) # Поля, физические единицы и замечания полностью определяет имеющийся парсер.
if profile == 'balsam' or profile == 'auto' and balsam.is_balsam_id(ident): # В автоматическом режиме Balsam распознаётся только по его документированным идентификаторам.
return _balsam(ident, payload) # Включает отдельную пару CAN ID пульта, присутствующую в общей библиотеке.
if profile == 'legacy': # Старые проектные форматы требуют явного проекта, поскольку диапазоны ID могут пересекаться.
selected = next((item for item in legacycan.PROJECTS if item.name == project), None) # Поиск использует общий каталог без копирования ID в анализатор.
if selected is None: # Ошибка выбора не должна давать расшифровку другого проекта.
raise ValueError('Unknown legacy CAN project: ' + project) # Сообщение остаётся видимым на дорожке ошибок.
node = selected.node_for(ident) # Маршрутизация зависит от формата: точный ID либо старшие биты ID.
if node is None: # На общей шине допустимы кадры других устройств и проектов.
return None # Чужие ID остаются только в дорожках базового CAN-декодера.
packet = legacycan.decode(selected.format, node, ident, payload) # Общая реализация восстанавливает адрес, маску и слова с нужным порядком байтов.
if packet is None: # Известный ID с неподходящим DLC обозначает повреждённый кадр выбранного проекта.
raise ValueError('Invalid legacy CAN payload') # Некорректные данные не выдаются как достоверные регистры.
registers = list(packet.present_values) # Получает только отмеченные маской слова в исходном адресном порядке.
commands = ['%s: %s' % ('R%04X' % address, ', '.join(selected.command_names[bit] for bit in range(16) if value & (1 << bit))) for address, value in registers if address == 127] # Регистр 127 хранит 16 командных битов с названиями выбранного проекта.
summary = '%s · %s · %s · %s' % (project, node.name, packet.source.value, ' '.join('R%04X=%04X' % pair for pair in registers)) # Краткая подпись связывает проект, устройство, направление и значения слов.
return dict(protocol='Legacy CAN', summary=summary, fields=[('Проект', project), ('Устройство', node.name), ('Направление', packet.source.value)] + [('Команды', text) for text in commands], registers=registers, warnings=[]) # Все подробности доступны на самостоятельных дорожках DSView.
if profile == 'ump-protocan' and ump_map == 'gas167': # Текущая карта 167/v1 назначается вручную и не смешивается с новыми глобальными ID.
if ump_node not in (1, 2): # В профиле имеются два УМП; номер не извлекается из локального адреса регистра.
raise ValueError('UMP node must be 1 or 2') # Ошибочный номер узла должен быть исправлен в настройках.
return _decoded(ump_can.decode_gas167(frame, ump_node)) # Имена параметров и активные биты берутся из действующей карты templates.
if profile == 'ump-protocan' and ump_map != 'assigned': # Назначенная глобальная карта обрабатывается штатным ProtoCAN ниже.
raise ValueError('Unknown UMP map: ' + ump_map) # Неизвестную карту нельзя интерпретировать как другую раскладку регистров.
from .set_can import legacy # Поздний импорт сохраняет совместимость существующего ядра и предотвращает циклическую загрузку модулей.
result = legacy(ident, payload) # Сохраняет строгие проверки DLC GAS/Boot, сегментных длин и старые структурированные поля событий.
application = _decoded(pc.decode(ident, payload)) # Общий ProtoCAN добавляет имена устройств, GAS, Settings, дискретов и служебных полей.
result.update(fields=application['fields'], warnings=application['warnings'], summary=result['summary'] + ' · ' + application['summary']) # Старые поля сохраняются, а полная прикладная расшифровка становится доступной на осциллограмме.
return result # Кадр выдаётся только после проверки wire-формата обоими существующими уровнями.
@@ -0,0 +1,29 @@
from collections import deque # Кольцевой буфер хранит только координаты одного максимального кадра.
from . import template_protocols # Подключает поставляемые пакеты templates в отдельном Python-процессе DSView.
from set_devices import tms_terminal as tms # Раскладки, CRC и потоковый поиск границ используются из действующего парсера терминала.
LAYOUTS = tuple(tms.PRESETS) + ('custom',) # Имена готовых раскладок совпадают с настройками терминала SETGUI.
class TmsStreamParser: # Адаптирует общий байтовый парсер к событиям с координатами sigrok.
def __init__(self, preset=tms.DEFAULT_PRESET, digit_bytes=38, analog_count=34, crc='MODBUS'): # Значения 38/34 соответствуют штатной раскладке message2.h.
selected = tms.PRESETS[preset] if preset != 'custom' else tms.Layout(digit_bytes, analog_count, crc) # Индивидуальная раскладка проверяется существующей моделью Layout.
self.layout = tms.Layout(selected.digit_bytes, selected.analog_count, crc) # CRC выбирается отдельно, поскольку в источниках есть MODBUS и CCITT-FALSE.
self.parser = tms.TmsParser(self.layout) # Поиск границ и подтверждение CRC выполняет общий парсер.
self.positions = deque(maxlen=self.layout.frame_size) # Число хранимых координат ограничено длиной одного кадра в байтах.
def flush(self): # Пауза либо ошибка UART запрещает объединять части разных посылок.
self.parser.reset() # Общий парсер отбрасывает недособранные байты и состояние синхронизации.
self.positions.clear() # Координаты старого хвоста также исключаются из следующего события.
return [] # Формат без сигнатуры не позволяет отличить неполный кадр от произвольного шума.
def feed(self, value, ss, es): # Один UART-байт приходит вместе с точными границами в отсчётах.
self.positions.append((ss, es)) # Последние frame_size позиций соответствуют окну поиска общего парсера.
events = [] # За один байт может завершиться один кадр фиксированной длины.
for record in self.parser.feed(bytes([value])): # Только CRC-подтверждённые записи выдаются прикладному слою.
fields = [('Команда', record.command), ('Адрес', str(record.address)), ('CRC', '%04X' % record.crc_frame)] # Служебные поля берутся из готовой модели TmsRecord.
fields += [(tms.analog_name(index), str(word)) for index, word in enumerate(record.analogs)] # Имена аналогов и знаковые int16 совпадают с терминалом SETGUI.
fields += [('Дискреты', ', '.join(record.set_bits()) or 'нет активных битов')] # Активные биты выдаются с исходной нумерацией байтов и битов.
result = dict(protocol='TMS UART', summary=record.summary, payload=record.raw[2:-3], raw=record.raw, fields=fields, analogs=record.analogs, digits=record.digits) # CRC и завершающий байт исключены из полезной нагрузки по контракту TMS_TO_TERMINAL.
events.append((self.positions[0][0], es, result, None)) # Начало подтверждённого окна сохраняется даже после пропущенного шума.
self.positions.clear() # Координаты завершённого кадра не удерживаются до следующей посылки.
return events # События имеют тот же контракт, что парсер SET UART.

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